diff --git a/js/provider-model-controls.js b/js/provider-model-controls.js
new file mode 100644
index 00000000..cb9d7ba3
--- /dev/null
+++ b/js/provider-model-controls.js
@@ -0,0 +1,200 @@
+// provider-model-controls.js - provider model dropdowns, pricing, and custom model selection.
+
+import { escapeHTML, showNotification } from './utils.js';
+import {
+ fetchOpenRouterModelPricing,
+ getCustomApiModel,
+ getOpenRouterModel,
+ getPpqModel,
+ getRoutstrModel,
+ getVeniceE2EE,
+ getVeniceModel,
+ renderModelPricingHint,
+ setCustomApiModel,
+ setOpenRouterModel,
+ setPpqModel,
+ setRoutstrModel,
+ setVeniceE2EE,
+ setVeniceModel,
+} from './api.js';
+import { buildModelOptions } from './provider-panel-renderers.js';
+
+export function updateVeniceModelPricing(modelId) {
+ const el = document.getElementById('venice-model-pricing');
+ if (el) el.innerHTML = renderModelPricingHint('venice', modelId || getVeniceModel());
+}
+
+export function renderVeniceModelDropdown(models) {
+ const area = document.getElementById('venice-model-area');
+ if (!area || !models.length) return;
+ const currentModel = getVeniceModel();
+ const opts = buildModelOptions('venice', models, currentModel, function(m) { return m.name || m.id; });
+ area.innerHTML = '' +
+ '' +
+ '
' + renderModelPricingHint('venice', currentModel) + '
';
+}
+
+export function onVeniceModelDropdownChange(value) {
+ const previous = getVeniceModel();
+ setVeniceModel(value);
+ localStorage.setItem(getVeniceE2EE() ? 'labcharts-venice-model-e2ee' : 'labcharts-venice-model-regular', value);
+ if (previous !== value) window.clearE2EESession?.();
+ updateVeniceModelPricing(value);
+}
+
+export function toggleVeniceE2EE(on) {
+ setVeniceE2EE(on);
+ if (!on) window.clearE2EESession?.();
+ // Swap model dropdown to E2EE or regular model list.
+ const listKey = on ? 'labcharts-venice-e2ee-models' : 'labcharts-venice-models';
+ let models = []; try { models = JSON.parse(localStorage.getItem(listKey) || '[]'); } catch {}
+ if (models.length) {
+ const prevKey = on ? 'labcharts-venice-model-regular' : 'labcharts-venice-model-e2ee';
+ const restoreKey = on ? 'labcharts-venice-model-e2ee' : 'labcharts-venice-model-regular';
+ localStorage.setItem(prevKey, getVeniceModel());
+ const restored = localStorage.getItem(restoreKey);
+ const newModel = restored && models.some(m => m.id === restored) ? restored : models[0].id;
+ setVeniceModel(newModel);
+ renderVeniceModelDropdown(models);
+ }
+ const el = document.getElementById('venice-e2ee-indicator');
+ if (el) el.style.display = on ? '' : 'none';
+ window.updateChatHeaderModel?.();
+ window.refreshWebSearchToggle?.();
+}
+
+export function updateOpenRouterModelPricing(modelId) {
+ const el = document.getElementById('openrouter-model-pricing');
+ if (el) el.innerHTML = renderModelPricingHint('openrouter', modelId || getOpenRouterModel());
+}
+
+export function renderOpenRouterModelDropdown(models) {
+ const area = document.getElementById('openrouter-model-area');
+ if (!area || !models.length) return;
+ const currentModel = getOpenRouterModel();
+ const isCustom = !models.some(m => m.id === currentModel);
+ const opts = buildModelOptions('openrouter', models, currentModel, function(m) { return m.name || m.id; });
+ area.innerHTML = '' +
+ '' +
+ '
' +
+ 'Press Enter to apply — checks model connectivity' +
+ '' + renderModelPricingHint('openrouter', currentModel) + '
';
+}
+
+export async function applyCustomOpenRouterModel(modelId) {
+ const id = modelId.trim();
+ if (!id) return;
+ setOpenRouterModel(id);
+ const pricingEl = document.getElementById('openrouter-model-pricing');
+ if (pricingEl) pricingEl.innerHTML = 'Checking pricing\u2026';
+ const select = document.getElementById('openrouter-model-select');
+ const input = document.getElementById('openrouter-custom-model');
+ const inDropdown = select && [...select.options].some(o => o.value === id);
+ if (select) {
+ if (inDropdown) {
+ select.value = id;
+ if (input) { input.value = ''; input.style.borderColor = ''; }
+ } else {
+ let customOpt = select.querySelector('option[value="__custom"]');
+ if (!customOpt) {
+ customOpt = document.createElement('option');
+ customOpt.value = '__custom';
+ customOpt.disabled = true;
+ customOpt.textContent = 'Using custom model';
+ select.insertBefore(customOpt, select.firstChild);
+ }
+ customOpt.selected = true;
+ }
+ }
+ const indicator = document.getElementById('openrouter-model-health');
+ if (indicator) { indicator.textContent = '\u23f3'; indicator.title = 'Checking...'; indicator.style.color = 'var(--text-muted)'; }
+ try {
+ await window.callClaudeAPI({ messages: [{ role: 'user', content: 'hi' }], maxTokens: 1 });
+ if (indicator) { indicator.textContent = '\u2713'; indicator.title = 'Model responding'; indicator.style.color = 'var(--green)'; }
+ if (input && !inDropdown) input.style.borderColor = 'var(--green)';
+ showNotification('Model set: ' + id, 'info');
+ await fetchOpenRouterModelPricing(id);
+ updateOpenRouterModelPricing(id);
+ } catch (e) {
+ if (indicator) { indicator.textContent = '\u2717'; indicator.title = e.message || 'Connection failed'; indicator.style.color = 'var(--red)'; }
+ if (input) input.style.borderColor = 'var(--red)';
+ updateOpenRouterModelPricing(id);
+ showNotification('Model check failed: ' + (e.message || 'unknown error'), 'error');
+ }
+}
+
+export function onOpenRouterDropdownChange(value) {
+ setOpenRouterModel(value);
+ updateOpenRouterModelPricing(value);
+ const input = document.getElementById('openrouter-custom-model');
+ if (input) { input.value = ''; input.style.borderColor = ''; }
+ const health = document.getElementById('openrouter-model-health');
+ if (health) { health.textContent = ''; health.title = ''; }
+ const select = document.getElementById('openrouter-model-select');
+ const customOpt = select?.querySelector('option[value="__custom"]');
+ if (customOpt) customOpt.remove();
+}
+
+export function updateRoutstrModelPricing(modelId) {
+ const el = document.getElementById('routstr-model-pricing');
+ if (el) el.innerHTML = renderModelPricingHint('routstr', modelId || getRoutstrModel());
+}
+
+export function renderRoutstrModelDropdown(models) {
+ const area = document.getElementById('routstr-model-area');
+ if (!area || !models.length) return;
+ let currentModel = getRoutstrModel();
+ const modelIds = models.map(m => m.id);
+ if (currentModel && !modelIds.includes(currentModel)) {
+ currentModel = modelIds[0];
+ setRoutstrModel(currentModel);
+ }
+ const opts = buildModelOptions('routstr', models, currentModel, function(m) { return m.name || m.id; });
+ area.innerHTML = '' +
+ '' +
+ '' + renderModelPricingHint('routstr', currentModel) + '
';
+}
+
+export function renderPpqModelDropdown(models) {
+ const area = document.getElementById('ppq-model-area');
+ if (!area || !models.length) return;
+ const currentModel = getPpqModel();
+ const opts = buildModelOptions('ppq', models, currentModel, function(m) { return m.name || m.id; });
+ area.innerHTML = '' +
+ '' +
+ '' + renderModelPricingHint('ppq', currentModel) + '
';
+}
+
+export function updatePpqModelPricing(modelId) {
+ const el = document.getElementById('ppq-model-pricing');
+ if (el) el.innerHTML = renderModelPricingHint('ppq', modelId);
+}
+
+export function renderCustomApiModelDropdown(models) {
+ const area = document.getElementById('custom-model-area');
+ if (!area) return;
+ const currentModel = getCustomApiModel();
+ const opts = buildModelOptions('custom', models, currentModel, function(m) { return m.name || m.id; });
+ const isCustom = !models.some(m => m.id === currentModel) && currentModel;
+ area.innerHTML = `
+
+
+ ${renderModelPricingHint('custom', currentModel)}
`;
+}
+
+export function updateCustomModelPricing(modelId) {
+ const el = document.getElementById('custom-model-pricing');
+ if (el) el.innerHTML = renderModelPricingHint('custom', modelId || getCustomApiModel());
+}
+
+export function applyCustomApiManualModel() {
+ const input = document.getElementById('custom-manual-model');
+ if (!input) return;
+ const model = input.value.trim();
+ if (!model) { showNotification('Enter a model ID', 'error'); return; }
+ setCustomApiModel(model);
+ const select = document.getElementById('custom-model-select');
+ if (select) select.value = model;
+ updateCustomModelPricing(model);
+ showNotification('Model set to ' + model, 'success');
+}
diff --git a/js/provider-panels.js b/js/provider-panels.js
index b3b4a118..9638094b 100644
--- a/js/provider-panels.js
+++ b/js/provider-panels.js
@@ -1,21 +1,19 @@
-// provider-panels.js — AI provider settings behavior, model dropdowns, balance display, key validation
+// provider-panels.js - AI provider settings behavior, balance display, key validation, and wallet flows
import { escapeHTML, escapeAttr, showNotification, showConfirmDialog } from './utils.js';
import {
getVeniceKey, saveVeniceKey, getOpenRouterKey, saveOpenRouterKey, getAIProvider, setAIProvider,
- getVeniceModel, setVeniceModel, getOpenRouterModel, setOpenRouterModel,
getOllamaMainModel, setOllamaMainModel, getOllamaPIIModel, setOllamaPIIModel,
getOllamaPIIUrl, setOllamaPIIUrl,
validateVeniceKey, validateOpenRouterKey, fetchVeniceModels, fetchOpenRouterModels,
- renderModelPricingHint,
- fetchOpenRouterModelPricing, getOpenRouterBalance, getVeniceBalance,
- getRoutstrKey, saveRoutstrKey, getRoutstrModel, setRoutstrModel,
+ getOpenRouterBalance, getVeniceBalance,
+ getRoutstrKey, saveRoutstrKey,
fetchRoutstrModels, validateRoutstrKey, createRoutstrAccount,
setAIPaused,
- getVeniceE2EE, setVeniceE2EE,
+ getVeniceE2EE,
getCustomApiUrl, setCustomApiUrl, getCustomApiKey, saveCustomApiKey,
- getCustomApiModel, setCustomApiModel, fetchCustomApiModels, validateCustomApiKey,
- getPpqKey, savePpqKey, getPpqModel, setPpqModel,
+ fetchCustomApiModels, validateCustomApiKey,
+ getPpqKey, savePpqKey,
fetchPpqModels, validatePpqKey, createPpqAccount, getPpqBalance, savePpqCreditId,
createPpqTopup, checkPpqTopupStatus,
rememberOpenRouterOAuthPreviousProvider, clearOpenRouterOAuthSession
@@ -24,7 +22,24 @@ import { getOllamaConfig, checkOllama, checkOpenAICompatible, saveOllamaConfig,
import { detectHardware, assessModel, assessFitness, getBestModel, getUpgradeSuggestion, saveHardwareOverride, getHardwareOverride } from './hardware.js';
import { updateKeyCache, encryptedSetItem } from './crypto.js';
import { ensureQRCode } from './provider-qr.js';
-import { renderAIProviderPanel, buildModelOptions } from './provider-panel-renderers.js';
+import { renderAIProviderPanel } from './provider-panel-renderers.js';
+import {
+ applyCustomApiManualModel,
+ applyCustomOpenRouterModel,
+ onOpenRouterDropdownChange,
+ onVeniceModelDropdownChange,
+ renderCustomApiModelDropdown,
+ renderOpenRouterModelDropdown,
+ renderPpqModelDropdown,
+ renderRoutstrModelDropdown,
+ renderVeniceModelDropdown,
+ toggleVeniceE2EE,
+ updateCustomModelPricing,
+ updateOpenRouterModelPricing,
+ updatePpqModelPricing,
+ updateRoutstrModelPricing,
+ updateVeniceModelPricing,
+} from './provider-model-controls.js';
import {
configureRoutstrWalletPanels,
clearRoutstrWalletTimers,
@@ -55,6 +70,23 @@ import {
} from './provider-wallet-panels.js';
export { renderAIProviderPanel } from './provider-panel-renderers.js';
+export {
+ applyCustomApiManualModel,
+ applyCustomOpenRouterModel,
+ onOpenRouterDropdownChange,
+ onVeniceModelDropdownChange,
+ renderCustomApiModelDropdown,
+ renderOpenRouterModelDropdown,
+ renderPpqModelDropdown,
+ renderRoutstrModelDropdown,
+ renderVeniceModelDropdown,
+ toggleVeniceE2EE,
+ updateCustomModelPricing,
+ updateOpenRouterModelPricing,
+ updatePpqModelPricing,
+ updateRoutstrModelPricing,
+ updateVeniceModelPricing,
+} from './provider-model-controls.js';
export {
refreshCashuWalletBalance,
@@ -563,10 +595,6 @@ export function refreshVeniceBalance() {
else if (el) el.textContent = 'Balance: unavailable';
});
}
-export function updateVeniceModelPricing(modelId) {
- const el = document.getElementById('venice-model-pricing');
- if (el) el.innerHTML = renderModelPricingHint('venice', modelId || getVeniceModel());
-}
export async function handleSaveVeniceKey() {
const input = document.getElementById('venice-key-input');
@@ -612,55 +640,10 @@ export function handleRemoveVeniceKey() {
window.openSettingsModal?.();
}
-export function renderVeniceModelDropdown(models) {
- const area = document.getElementById('venice-model-area');
- if (!area || !models.length) return;
- const currentModel = getVeniceModel();
- const opts = buildModelOptions('venice', models, currentModel, function(m) { return m.name || m.id; });
- area.innerHTML = '' +
- '' +
- '' + renderModelPricingHint('venice', currentModel) + '
';
-}
-
-export function onVeniceModelDropdownChange(value) {
- const previous = getVeniceModel();
- setVeniceModel(value);
- localStorage.setItem(getVeniceE2EE() ? 'labcharts-venice-model-e2ee' : 'labcharts-venice-model-regular', value);
- if (previous !== value) window.clearE2EESession?.();
- updateVeniceModelPricing(value);
-}
-
-export function toggleVeniceE2EE(on) {
- setVeniceE2EE(on);
- if (!on) window.clearE2EESession?.();
- // Swap model dropdown to E2EE or regular model list
- const listKey = on ? 'labcharts-venice-e2ee-models' : 'labcharts-venice-models';
- let models = []; try { models = JSON.parse(localStorage.getItem(listKey) || '[]'); } catch {}
- if (models.length) {
- // Save current model for the mode we're leaving, restore the one for the mode we're entering
- const prevKey = on ? 'labcharts-venice-model-regular' : 'labcharts-venice-model-e2ee';
- const restoreKey = on ? 'labcharts-venice-model-e2ee' : 'labcharts-venice-model-regular';
- localStorage.setItem(prevKey, getVeniceModel());
- const restored = localStorage.getItem(restoreKey);
- const newModel = restored && models.some(m => m.id === restored) ? restored : models[0].id;
- setVeniceModel(newModel);
- renderVeniceModelDropdown(models);
- }
- const el = document.getElementById('venice-e2ee-indicator');
- if (el) el.style.display = on ? '' : 'none';
- window.updateChatHeaderModel?.();
- window.refreshWebSearchToggle?.();
-}
-
// ═══════════════════════════════════════════════
// OPENROUTER HANDLERS
// ═══════════════════════════════════════════════
-export function updateOpenRouterModelPricing(modelId) {
- const el = document.getElementById('openrouter-model-pricing');
- if (el) el.innerHTML = renderModelPricingHint('openrouter', modelId || getOpenRouterModel());
-}
-
export async function handleSaveOpenRouterKey() {
const input = document.getElementById('openrouter-key-input');
const btn = document.getElementById('save-openrouter-key-btn');
@@ -698,19 +681,6 @@ export function handleRemoveOpenRouterKey() {
window.openSettingsModal?.();
}
-export function renderOpenRouterModelDropdown(models) {
- const area = document.getElementById('openrouter-model-area');
- if (!area || !models.length) return;
- const currentModel = getOpenRouterModel();
- const isCustom = !models.some(m => m.id === currentModel);
- const opts = buildModelOptions('openrouter', models, currentModel, function(m) { return m.name || m.id; });
- area.innerHTML = '' +
- '' +
- '
' +
- 'Press Enter to apply — checks model connectivity' +
- '' + renderModelPricingHint('openrouter', currentModel) + '
';
-}
-
function _orBalanceHtml(remaining) {
const v = parseFloat(remaining);
const color = v < 0.10 ? 'var(--red)' : v < 0.50 ? 'var(--yellow, #f0a800)' : 'var(--green)';
@@ -763,75 +733,10 @@ export function showInsufficientBalanceDialog() {
overlay.onclick = function(e) { if (e.target === overlay) close(); };
}
-export async function applyCustomOpenRouterModel(modelId) {
- const id = modelId.trim();
- if (!id) return;
- setOpenRouterModel(id);
- // Show "checking..." while we verify the model
- const pricingEl = document.getElementById('openrouter-model-pricing');
- if (pricingEl) pricingEl.innerHTML = 'Checking pricing\u2026';
- const select = document.getElementById('openrouter-model-select');
- const input = document.getElementById('openrouter-custom-model');
- const inDropdown = select && [...select.options].some(o => o.value === id);
- if (select) {
- if (inDropdown) {
- select.value = id;
- if (input) { input.value = ''; input.style.borderColor = ''; }
- } else {
- // Show "Custom model" placeholder in dropdown
- let customOpt = select.querySelector('option[value="__custom"]');
- if (!customOpt) {
- customOpt = document.createElement('option');
- customOpt.value = '__custom';
- customOpt.disabled = true;
- customOpt.textContent = 'Using custom model';
- select.insertBefore(customOpt, select.firstChild);
- }
- customOpt.selected = true;
- }
- }
- // Health check — verify model responds
- const indicator = document.getElementById('openrouter-model-health');
- if (indicator) { indicator.textContent = '⏳'; indicator.title = 'Checking...'; indicator.style.color = 'var(--text-muted)'; }
- try {
- await window.callClaudeAPI({ messages: [{ role: 'user', content: 'hi' }], maxTokens: 1 });
- if (indicator) { indicator.textContent = '✓'; indicator.title = 'Model responding'; indicator.style.color = 'var(--green)'; }
- if (input && !inDropdown) input.style.borderColor = 'var(--green)';
- showNotification('Model set: ' + id, 'info');
- // Fetch actual pricing and update display
- await fetchOpenRouterModelPricing(id);
- updateOpenRouterModelPricing(id);
- } catch (e) {
- if (indicator) { indicator.textContent = '✗'; indicator.title = e.message || 'Connection failed'; indicator.style.color = 'var(--red)'; }
- if (input) input.style.borderColor = 'var(--red)';
- updateOpenRouterModelPricing(id);
- showNotification('Model check failed: ' + (e.message || 'unknown error'), 'error');
- }
-}
-
-export function onOpenRouterDropdownChange(value) {
- setOpenRouterModel(value);
- updateOpenRouterModelPricing(value);
- const input = document.getElementById('openrouter-custom-model');
- if (input) { input.value = ''; input.style.borderColor = ''; }
- const health = document.getElementById('openrouter-model-health');
- if (health) { health.textContent = ''; health.title = ''; }
- // Remove "Using custom model" placeholder if present
- const select = document.getElementById('openrouter-model-select');
- const customOpt = select?.querySelector('option[value="__custom"]');
- if (customOpt) customOpt.remove();
-}
-
-
// ─── Routstr mode toggle ───
// Direct mode removed — wallet-only
// ─── Routstr handlers ───
-export function updateRoutstrModelPricing(modelId) {
- const el = document.getElementById('routstr-model-pricing');
- if (el) el.innerHTML = renderModelPricingHint('routstr', modelId || getRoutstrModel());
-}
-
export async function handleSaveRoutstrKey() {
const input = document.getElementById('routstr-key-input');
const btn = document.getElementById('save-routstr-key-btn');
@@ -904,22 +809,6 @@ export function handleRemoveRoutstrKey() {
window.openSettingsModal?.();
}
-export function renderRoutstrModelDropdown(models) {
- const area = document.getElementById('routstr-model-area');
- if (!area || !models.length) return;
- let currentModel = getRoutstrModel();
- // Auto-select first model if stored model isn't available on this node
- const modelIds = models.map(m => m.id);
- if (currentModel && !modelIds.includes(currentModel)) {
- currentModel = modelIds[0];
- setRoutstrModel(currentModel);
- }
- const opts = buildModelOptions('routstr', models, currentModel, function(m) { return m.name || m.id; });
- area.innerHTML = '' +
- '' +
- '' + renderModelPricingHint('routstr', currentModel) + '
';
-}
-
// ─── PPQ handlers ───
let _ppqCreating = false;
export async function handleCreatePpqAccount() {
@@ -1021,21 +910,6 @@ export async function handleRemovePpqKey() {
}
}
-export function renderPpqModelDropdown(models) {
- const area = document.getElementById('ppq-model-area');
- if (!area || !models.length) return;
- const currentModel = getPpqModel();
- const opts = buildModelOptions('ppq', models, currentModel, function(m) { return m.name || m.id; });
- area.innerHTML = '' +
- '' +
- '' + renderModelPricingHint('ppq', currentModel) + '
';
-}
-
-export function updatePpqModelPricing(modelId) {
- const el = document.getElementById('ppq-model-pricing');
- if (el) el.innerHTML = renderModelPricingHint('ppq', modelId);
-}
-
function _ppqBalanceHtml(balance) {
const v = parseFloat(balance);
const color = v < 0.10 ? 'var(--red)' : v < 0.50 ? 'var(--yellow, #f0a800)' : 'var(--green)';
@@ -1274,37 +1148,6 @@ function handleRemoveCustomApi() {
if (panel) panel.innerHTML = renderAIProviderPanel('custom');
}
-function renderCustomApiModelDropdown(models) {
- const area = document.getElementById('custom-model-area');
- if (!area) return;
- const currentModel = getCustomApiModel();
- const opts = buildModelOptions('custom', models, currentModel, function(m) { return m.name || m.id; });
- const isCustom = !models.some(m => m.id === currentModel) && currentModel;
- area.innerHTML = `
-
-
- ${renderModelPricingHint('custom', currentModel)}
`;
-}
-
-function updateCustomModelPricing(modelId) {
- const el = document.getElementById('custom-model-pricing');
- if (el) el.innerHTML = renderModelPricingHint('custom', modelId || getCustomApiModel());
-}
-
-function applyCustomApiManualModel() {
- const input = document.getElementById('custom-manual-model');
- if (!input) return;
- const model = input.value.trim();
- if (!model) { showNotification('Enter a model ID', 'error'); return; }
- setCustomApiModel(model);
- // Update dropdown if it exists
- const select = document.getElementById('custom-model-select');
- if (select) select.value = model;
- updateCustomModelPricing(model);
- showNotification('Model set to ' + model, 'success');
-}
-
-
// ─── Model advisor helpers ───
function refreshModelAdvisor() {
const details = window._lastOllamaModelDetails || [];
diff --git a/service-worker.js b/service-worker.js
index 860598d7..8bffb9be 100755
--- a/service-worker.js
+++ b/service-worker.js
@@ -188,6 +188,7 @@ const APP_SHELL = [
'/js/provider-qr.js',
'/js/provider-wallet-panels.js',
'/js/provider-panel-renderers.js',
+ '/js/provider-model-controls.js',
'/js/provider-panels.js',
'/js/dna.js',
'/js/hardware.js',
diff --git a/tests/test-audit.js b/tests/test-audit.js
index 60616102..f2133b1c 100644
--- a/tests/test-audit.js
+++ b/tests/test-audit.js
@@ -58,6 +58,7 @@ const swAuditSrc = read('service-worker.js');
assert('SW uses importScripts for version', swAuditSrc.includes("importScripts('/version.js')"));
assert('SW CACHE_NAME uses semver', swAuditSrc.includes('`labcharts-v${self.APP_VERSION}`'));
assert('SW APP_SHELL includes API provider storage module', swAuditSrc.includes("'/js/api-provider-storage.js'"));
+assert('SW APP_SHELL includes provider model controls module', swAuditSrc.includes("'/js/provider-model-controls.js'"));
assert('SW APP_SHELL includes API transport module', swAuditSrc.includes("'/js/api-transport.js'"));
assert('SW APP_SHELL includes PDF import review module', swAuditSrc.includes("'/js/pdf-import-review.js'"));
assert('SW APP_SHELL includes PDF import support modules',
diff --git a/tests/test-custom-api.js b/tests/test-custom-api.js
index dbfb580c..cd07687d 100644
--- a/tests/test-custom-api.js
+++ b/tests/test-custom-api.js
@@ -217,7 +217,8 @@ console.log('\n12. settings.js + provider panel source inspection');
const settingsSrc = read('js/settings.js');
const panelsSrc = read('js/provider-panels.js');
const panelRenderSrc = read('js/provider-panel-renderers.js');
-const providerUiSrc = panelsSrc + panelRenderSrc;
+const providerModelControlsSrc = read('js/provider-model-controls.js');
+const providerUiSrc = panelsSrc + panelRenderSrc + providerModelControlsSrc;
assert('settings.js has data-provider="custom" button', settingsSrc.includes('data-provider="custom"'));
assert("settings.js wires switchAIProvider('custom')", settingsSrc.includes("switchAIProvider('custom')"));
assert('provider code imports getCustomApiUrl', providerUiSrc.includes('getCustomApiUrl'));
@@ -225,14 +226,14 @@ assert('provider-panels imports setCustomApiUrl', panelsSrc.includes('setCustomA
assert('provider code imports getCustomApiKey', providerUiSrc.includes('getCustomApiKey'));
assert('provider-panels imports saveCustomApiKey', panelsSrc.includes('saveCustomApiKey'));
assert('provider code imports getCustomApiModel', providerUiSrc.includes('getCustomApiModel'));
-assert('provider-panels imports setCustomApiModel', panelsSrc.includes('setCustomApiModel'));
+assert('provider-model-controls imports setCustomApiModel', providerModelControlsSrc.includes('setCustomApiModel'));
assert('provider-panels imports fetchCustomApiModels', panelsSrc.includes('fetchCustomApiModels'));
assert('provider-panels imports validateCustomApiKey', panelsSrc.includes('validateCustomApiKey'));
assert('renderAIProviderPanel handles custom', panelRenderSrc.includes("provider === 'custom'"));
assert('handleSaveCustomApi exists', panelsSrc.includes('function handleSaveCustomApi()'));
assert('handleRemoveCustomApi exists', panelsSrc.includes('function handleRemoveCustomApi()'));
-assert('renderCustomApiModelDropdown exists', panelsSrc.includes('function renderCustomApiModelDropdown('));
-assert('applyCustomApiManualModel exists', panelsSrc.includes('function applyCustomApiManualModel()'));
+assert('renderCustomApiModelDropdown exists', providerModelControlsSrc.includes('function renderCustomApiModelDropdown('));
+assert('applyCustomApiManualModel exists', providerModelControlsSrc.includes('function applyCustomApiManualModel()'));
assert('custom-url-input element', panelRenderSrc.includes('custom-url-input'));
assert('custom-key-input element', panelRenderSrc.includes('custom-key-input'));
assert('custom-model-area element', providerUiSrc.includes('custom-model-area'));
diff --git a/tests/test-openrouter.js b/tests/test-openrouter.js
index 49d1cfce..10ca54e4 100644
--- a/tests/test-openrouter.js
+++ b/tests/test-openrouter.js
@@ -98,11 +98,12 @@ else localStorage.removeItem('labcharts-openrouter-pricing');
console.log('\n3. provider panel source inspection');
const ppSrc = read('js/provider-panels.js');
const providerRenderSrc = read('js/provider-panel-renderers.js');
-const providerUiSrc = ppSrc + providerRenderSrc;
+const providerModelControlsSrc = read('js/provider-model-controls.js');
+const providerUiSrc = ppSrc + providerRenderSrc + providerModelControlsSrc;
assert('imports getOpenRouterKey', providerUiSrc.includes('getOpenRouterKey'));
assert('imports saveOpenRouterKey', ppSrc.includes('saveOpenRouterKey'));
assert('imports getOpenRouterModel', providerUiSrc.includes('getOpenRouterModel'));
-assert('imports setOpenRouterModel', ppSrc.includes('setOpenRouterModel'));
+assert('imports setOpenRouterModel', providerModelControlsSrc.includes('setOpenRouterModel'));
assert('imports getOpenRouterModelDisplay', providerRenderSrc.includes('getOpenRouterModelDisplay'));
assert('imports validateOpenRouterKey', ppSrc.includes('validateOpenRouterKey'));
assert('imports fetchOpenRouterModels', ppSrc.includes('fetchOpenRouterModels'));
@@ -114,8 +115,8 @@ assert('eager provider bridge persists selection synchronously', settingsSrc.inc
assert('renderAIProviderPanel handles openrouter', providerRenderSrc.includes("provider === 'openrouter'"));
assert('handleSaveOpenRouterKey exists', ppSrc.includes('function handleSaveOpenRouterKey()'));
assert('handleRemoveOpenRouterKey exists', ppSrc.includes('function handleRemoveOpenRouterKey()'));
-assert('renderOpenRouterModelDropdown exists', ppSrc.includes('function renderOpenRouterModelDropdown('));
-assert('updateOpenRouterModelPricing exists', ppSrc.includes('function updateOpenRouterModelPricing('));
+assert('renderOpenRouterModelDropdown exists', providerModelControlsSrc.includes('function renderOpenRouterModelDropdown('));
+assert('updateOpenRouterModelPricing exists', providerModelControlsSrc.includes('function updateOpenRouterModelPricing('));
assert('openrouter-key-input element', providerRenderSrc.includes('openrouter-key-input'));
assert('openrouter-model-area element', providerUiSrc.includes('openrouter-model-area'));
assert('openrouter-model-pricing element', providerUiSrc.includes('openrouter-model-pricing'));
@@ -181,6 +182,7 @@ assert('SW uses importScripts for version', swSrc.includes("importScripts('/vers
assert('SW CACHE_NAME uses semver', swSrc.includes('`labcharts-v${self.APP_VERSION}`'));
assert('SW bypasses openrouter.ai', swSrc.includes('openrouter.ai'));
assert('SW caches provider-panel-renderers.js', swSrc.includes('/js/provider-panel-renderers.js'));
+assert('SW caches provider-model-controls.js', swSrc.includes('/js/provider-model-controls.js'));
// ─── 7. Window function exports ───
console.log('\n7. Window function exports');
diff --git a/tests/test-venice-e2ee.js b/tests/test-venice-e2ee.js
index 9705f897..38e96db6 100644
--- a/tests/test-venice-e2ee.js
+++ b/tests/test-venice-e2ee.js
@@ -361,11 +361,12 @@ try {
// 16. Settings + Chat source checks
const providerSrc = read('js/provider-panels.js');
const providerRenderSrc = read('js/provider-panel-renderers.js');
+const providerModelControlsSrc = read('js/provider-model-controls.js');
assert('provider renderer has venice-e2ee-toggle', providerRenderSrc.includes('venice-e2ee-toggle'));
assert('provider renderer has venice-e2ee-indicator', providerRenderSrc.includes('venice-e2ee-indicator'));
-assert('provider-panels has toggleVeniceE2EE', providerSrc.includes('toggleVeniceE2EE'));
-assert('provider-panels has Venice model change handler', providerSrc.includes('function onVeniceModelDropdownChange'));
-assert('Venice model dropdown uses change handler', (providerSrc + providerRenderSrc).includes('onchange="onVeniceModelDropdownChange(this.value)"'));
+assert('provider model controls has toggleVeniceE2EE', providerModelControlsSrc.includes('function toggleVeniceE2EE'));
+assert('provider model controls has Venice model change handler', providerModelControlsSrc.includes('function onVeniceModelDropdownChange'));
+assert('Venice model dropdown uses change handler', (providerSrc + providerRenderSrc + providerModelControlsSrc).includes('onchange="onVeniceModelDropdownChange(this.value)"'));
const chatSrc = read('js/chat.js');
const chatSendSrc = read('js/chat-send.js');
const chatAttestationSrc = read('js/chat-attestation.js');
diff --git a/version.js b/version.js
index 56270ccd..da7a7af8 100644
--- a/version.js
+++ b/version.js
@@ -2,4 +2,4 @@
// Classic script (not ES module) so it works in both browser and service worker.
// Browser: