From 5d748c5b048ba959fe6aa14dabdf49a8785646b9 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Fri, 3 Apr 2026 10:49:36 +0200 Subject: [PATCH] Add Gemini option --- src/ifcchat/README.md | 2 +- src/ifcchat/api_openai.js | 8 +++++-- src/ifcchat/api_openrouter.js | 8 +++++-- src/ifcchat/app.js | 45 ++++++++++++++++++++++++++++++++++- src/ifcchat/index.html | 6 +++++ 5 files changed, 63 insertions(+), 6 deletions(-) diff --git a/src/ifcchat/README.md b/src/ifcchat/README.md index b2cceecc52..5dc434143a 100644 --- a/src/ifcchat/README.md +++ b/src/ifcchat/README.md @@ -1,7 +1,7 @@ IfcOpenShell AI Assistant ========================= -A web-based client-side (pyodide + OpenAI, Anthropic, or OpenRouter API) model interrogation and generation API based on: ifcedit, ifcquery and ifcmcp (ifcopenshell-mcp) packaged in a HTML+JS application. +A web-based client-side (pyodide + OpenAI, Anthropic, Gemini, or OpenRouter API) model interrogation and generation API based on: ifcedit, ifcquery and ifcmcp (ifcopenshell-mcp) packaged in a HTML+JS application. ### Setup instructions diff --git a/src/ifcchat/api_openai.js b/src/ifcchat/api_openai.js index 0fa6d7a88f..8903f897e4 100644 --- a/src/ifcchat/api_openai.js +++ b/src/ifcchat/api_openai.js @@ -1,6 +1,10 @@ +function getChatCompletionsUrl(baseURL) { + const root = (baseURL || "https://api.openai.com/v1").replace(/\/+$/, ""); + return `${root}/chat/completions`; +} -export async function chat({ apiKey, model, messages, tools }) { - const res = await fetch("https://api.openai.com/v1/chat/completions", { +export async function chat({ apiKey, baseURL, model, messages, tools }) { + const res = await fetch(getChatCompletionsUrl(baseURL), { method: "POST", headers: { "Content-Type": "application/json", diff --git a/src/ifcchat/api_openrouter.js b/src/ifcchat/api_openrouter.js index 9a02ee3025..38c11e36e8 100644 --- a/src/ifcchat/api_openrouter.js +++ b/src/ifcchat/api_openrouter.js @@ -1,6 +1,10 @@ +function getChatCompletionsUrl(baseURL) { + const root = (baseURL || "https://openrouter.ai/api/v1").replace(/\/+$/, ""); + return `${root}/chat/completions`; +} -export async function chat({ apiKey, model, messages, tools }) { - const res = await fetch("https://openrouter.ai/api/v1/chat/completions", { +export async function chat({ apiKey, baseURL, model, messages, tools }) { + const res = await fetch(getChatCompletionsUrl(baseURL), { method: "POST", headers: { "Content-Type": "application/json", diff --git a/src/ifcchat/app.js b/src/ifcchat/app.js index 22a9b0a055..28a20f1cb7 100644 --- a/src/ifcchat/app.js +++ b/src/ifcchat/app.js @@ -8,6 +8,9 @@ const PROVIDERS = { api: openaiApi, apiKeyLabel: "OpenAI API key", apiKeyPlaceholder: "sk-...", + baseUrlLabel: "Base URL", + baseUrlPlaceholder: "https://api.openai.com/v1", + baseUrlDefault: "https://api.openai.com/v1", models: [ { value: "gpt-5", @@ -38,10 +41,35 @@ const PROVIDERS = { }, ], }, + gemini: { + api: openaiApi, + apiKeyLabel: "Gemini API key", + apiKeyPlaceholder: "AIza...", + baseUrlLabel: "Base URL", + baseUrlPlaceholder: "https://generativelanguage.googleapis.com/v1beta/openai/", + baseUrlDefault: "https://generativelanguage.googleapis.com/v1beta/openai/", + models: [ + { + value: "gemini-3-flash-preview", + label: "gemini-3-flash-preview" + }, + { + value: "gemini-2.5-flash", + label: "gemini-2.5-flash" + }, + { + value: "gemini-2.5-pro", + label: "gemini-2.5-pro" + }, + ], + }, openrouter: { api: openrouterApi, apiKeyLabel: "OpenRouter API key", apiKeyPlaceholder: "sk-or-v1-...", + baseUrlLabel: "Base URL", + baseUrlPlaceholder: "https://openrouter.ai/api/v1", + baseUrlDefault: "https://openrouter.ai/api/v1", models: [ { value: "openai/gpt-oss-20b", @@ -79,6 +107,9 @@ const sendBtn = $("send"); const inputEl = $("input"); const apiKeyEl = $("apiKey"); const apiKeyLabelEl = $("apiKeyLabel"); +const baseUrlRowEl = $("baseUrlRow"); +const baseUrlLabelEl = $("baseUrlLabel"); +const baseUrlEl = $("baseUrl"); const modelEl = $("model"); const providerEl = $("provider"); const ifcFileEl = $("ifcFile"); @@ -89,6 +120,15 @@ function onProviderChange() { const provider = PROVIDERS[providerEl.value]; apiKeyLabelEl.innerHTML = `${provider.apiKeyLabel}stored in browser memory; only sent to provider servers`; apiKeyEl.placeholder = provider.apiKeyPlaceholder; + baseUrlRowEl.hidden = !provider.baseUrlDefault; + if (provider.baseUrlDefault) { + baseUrlLabelEl.innerHTML = `${provider.baseUrlLabel}override the API endpoint for OpenAI-compatible providers`; + baseUrlEl.placeholder = provider.baseUrlPlaceholder; + baseUrlEl.value = provider.baseUrlDefault; + } else { + baseUrlEl.value = ""; + baseUrlEl.placeholder = ""; + } modelEl.innerHTML = provider.models.map(m => ``).join(""); } @@ -251,13 +291,16 @@ async function runAgentTurn(userText) { const apiKey = apiKeyEl.value.trim(); if (!apiKey) throw new Error("Missing API key"); - const { chat } = PROVIDERS[providerEl.value].api; + const provider = PROVIDERS[providerEl.value]; + const { chat } = provider.api; + const baseURL = provider.baseUrlDefault ? baseUrlEl.value.trim() : undefined; messages.push({ role: "user", content: userText }); for (let i = 0; i < 64; i++) { const response = await chat({ apiKey, + baseURL, model: modelEl.value, messages: [{ role: "system", content: SYSTEM_INSTRUCTIONS }, ...messages], tools, diff --git a/src/ifcchat/index.html b/src/ifcchat/index.html index 1ad3bc198d..d7f348d282 100644 --- a/src/ifcchat/index.html +++ b/src/ifcchat/index.html @@ -17,6 +17,7 @@ @@ -31,6 +32,11 @@ +
+ + +
+