/**
* Utilities for populating settings form controls and synchronizing model option values.
*/
/**
* Formats a thinking level value into a human-readable display label.
* @param {string|null} level - Raw thinking level value.
* @returns {string} Formatted label.
*/
export function formatThinkingLevelLabel(level) {
if (level === null || level === undefined || level === '' || level === 'none') {
return 'None';
}
const str = String(level);
switch (str.toUpperCase()) {
case 'MINIMAL':
return 'Minimal';
case 'LOW':
return 'Low';
case 'MEDIUM':
return 'Medium';
case 'HIGH':
return 'High';
case 'XHIGH':
return 'Extra High';
case 'MAX':
return 'Max';
default:
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}
}
/**
* Populates a select dropdown with presets.
* @param {HTMLSelectElement|null} selectEl - Target select element.
* @param {object} presets - Map of preset definitions.
* @param {string|null} [selectedKey=''] - Key of the preset to select.
*/
export function populatePresetDropdown(selectEl, presets, selectedKey = '') {
if (!selectEl) return;
selectEl.innerHTML = '';
if (!presets || typeof presets !== 'object') return;
const customOption = document.createElement('option');
customOption.value = '';
customOption.textContent = '(Custom / None)';
selectEl.appendChild(customOption);
for (const key in presets) {
if (Object.prototype.hasOwnProperty.call(presets, key)) {
const preset = presets[key];
const option = document.createElement('option');
option.value = key;
option.textContent = `${preset.name || key} (${preset.description || ''})`;
if (selectedKey && key === selectedKey) {
option.selected = true;
}
selectEl.appendChild(option);
}
}
if (!selectedKey) {
customOption.selected = true;
}
}
/**
* Populates a select dropdown with model families.
* @param {HTMLSelectElement|null} selectEl - Target select element.
* @param {object} familiesData - Map of family definitions.
* @param {string|null} [selectedFamily='gemini'] - Currently selected family key.
*/
export function populateFamilyDropdown(selectEl, familiesData, selectedFamily = 'gemini') {
if (!selectEl) return;
selectEl.innerHTML = '';
if (!familiesData || typeof familiesData !== 'object') return;
for (const key in familiesData) {
if (Object.prototype.hasOwnProperty.call(familiesData, key)) {
const family = familiesData[key];
const option = document.createElement('option');
option.value = key;
option.textContent = family.family || key;
if (key === selectedFamily) {
option.selected = true;
}
selectEl.appendChild(option);
}
}
}
/**
* Populates a select dropdown with model versions within a family.
* @param {HTMLSelectElement|null} selectEl - Target select element.
* @param {Array<object>} modelsList - Array of model definitions for the family.
* @param {string|null} [selectedModel=''] - Selected API model string.
*/
export function populateModelVersionDropdown(selectEl, modelsList, selectedModel = '') {
if (!selectEl) return;
selectEl.innerHTML = '';
if (!Array.isArray(modelsList)) return;
modelsList.forEach((m) => {
const option = document.createElement('option');
option.value = m.model;
option.textContent = m.suffix || m.model;
if (m.model === selectedModel) {
option.selected = true;
}
selectEl.appendChild(option);
});
}
/**
* Populates a select dropdown with thinking levels for a model.
* @param {HTMLSelectElement|null} selectEl - Target select element.
* @param {Array<string|null>} thinkingModes - Supported thinking mode values.
* @param {string|null} [selectedLevel=null] - Currently selected thinking level.
*/
export function populateThinkingLevelDropdown(selectEl, thinkingModes, selectedLevel = null) {
if (!selectEl) return;
selectEl.innerHTML = '';
if (!Array.isArray(thinkingModes)) return;
const normSelected = (selectedLevel === null || selectedLevel === undefined || selectedLevel === '' || selectedLevel === 'none') ? '' : String(selectedLevel);
thinkingModes.forEach((mode) => {
const val = (mode === null || mode === undefined || mode === '' || mode === 'none') ? '' : String(mode);
const option = document.createElement('option');
option.value = val;
option.textContent = formatThinkingLevelLabel(mode);
if (val === normSelected) {
option.selected = true;
}
selectEl.appendChild(option);
});
}
/**
* Finds a matching preset key from the given model settings.
* @param {object} presets - Presets dictionary.
* @param {string} familyKey - Model family key (gemini, openai, deepseek).
* @param {string} modelId - Model API identifier.
* @param {string|null} thinkingLevel - Thinking level string.
* @returns {string} Matching preset key or empty string if none.
*/
export function findMatchingPreset(presets, familyKey, modelId, thinkingLevel) {
if (!presets || typeof presets !== 'object') return '';
const normThinking = (thinkingLevel === null || thinkingLevel === undefined || thinkingLevel === '' || thinkingLevel === 'none') ? '' : String(thinkingLevel).toUpperCase();
for (const key in presets) {
if (Object.prototype.hasOwnProperty.call(presets, key)) {
const preset = presets[key];
const presetProvider = (preset.provider || '').toLowerCase();
const targetFamily = (familyKey || '').toLowerCase();
if (presetProvider === targetFamily && preset.model === modelId) {
const pThinking = (preset.thinking === null || preset.thinking === undefined || preset.thinking === '' || preset.thinking === 'none') ? '' : String(preset.thinking).toUpperCase();
if (pThinking === normThinking) {
return key;
}
}
}
}
return '';
}
/**
* Formats the full name for a model: [family] [suffix] [level] (e.g., "Gemini 3.1 Flash Lite Medium").
* @param {string} familyKey - Family key.
* @param {string} modelId - API model identifier.
* @param {string|null} thinkingLevel - Thinking level.
* @param {object} familiesData - Families data from models.json.
* @returns {string}
*/
export function formatModelFullName(familyKey, modelId, thinkingLevel, familiesData) {
const familyObj = familiesData?.[familyKey];
const familyName = familyObj?.family || (familyKey ? (familyKey.charAt(0).toUpperCase() + familyKey.slice(1)) : '');
let suffix = modelId;
if (familyObj?.models) {
const m = familyObj.models.find(item => item.model === modelId);
if (m?.suffix) {
suffix = m.suffix;
}
}
const levelLabel = (thinkingLevel !== null && thinkingLevel !== undefined && thinkingLevel !== '' && thinkingLevel !== 'none')
? formatThinkingLevelLabel(thinkingLevel)
: '';
return [familyName, suffix, levelLabel].filter(Boolean).join(' ');
}
/**
* Formats the short name for a model chip: [family] [short suffix] (e.g., "Gemini 3.1FL").
* @param {string} familyKey - Family key.
* @param {string} modelId - API model identifier.
* @param {object} familiesData - Families data from models.json.
* @returns {string}
*/
export function formatModelShortName(familyKey, modelId, familiesData) {
if (!modelId && !familyKey) return 'Model';
let familyName = '';
let shortSuffix = modelId || '';
if (familiesData && typeof familiesData === 'object') {
if (familyKey && familiesData[familyKey]) {
const fam = familiesData[familyKey];
familyName = fam.family || (familyKey.charAt(0).toUpperCase() + familyKey.slice(1));
if (fam.models) {
const m = fam.models.find(item => item.model === modelId || (modelId && item.model.includes(modelId)) || (modelId && modelId.includes(item.model)));
if (m) {
shortSuffix = m.short_suffix || m.suffix || shortSuffix;
}
}
} else {
for (const k in familiesData) {
const fam = familiesData[k];
const m = fam?.models?.find(item => item.model === modelId || (modelId && item.model.includes(modelId)) || (modelId && modelId.includes(item.model)));
if (m) {
familyName = fam.family || (k.charAt(0).toUpperCase() + k.slice(1));
shortSuffix = m.short_suffix || m.suffix || shortSuffix;
break;
}
}
}
}
if (!familyName && familyKey) {
familyName = familyKey.charAt(0).toUpperCase() + familyKey.slice(1);
}
return [familyName, shortSuffix].filter(Boolean).join(' ') || modelId || 'Model';
}
/**
* Populates a select dropdown with options from a models map (legacy fallback).
* @param {HTMLSelectElement|null} selectEl - Target select element.
* @param {object} models - Key-value map of model definitions.
* @param {string|null} [selectedKey=null] - Key of the option to mark selected.
*/
export function populateModelDropdown(selectEl, models, selectedKey = null) {
if (!selectEl) return;
selectEl.innerHTML = '';
if (!models || typeof models !== 'object') return;
for (const key in models) {
if (Object.prototype.hasOwnProperty.call(models, key)) {
const model = models[key];
const option = document.createElement('option');
option.value = key;
option.textContent = `${model.name || key} (${model.description || ''})`;
if (selectedKey && key === selectedKey) {
option.selected = true;
}
selectEl.appendChild(option);
}
}
}
/**
* Resolves [rate_input, rate_output] per 1M tokens for a given model identifier.
* @param {string} model - Model identifier.
* @returns {Array<number>} [rateInput, rateOutput]
*/
export function getModelRates(model) {
if (!model) return [0.30, 2.50];
const m = String(model).toLowerCase();
if (m.includes('gpt-6') && m.includes('astra')) return [10.00, 50.00];
if (m.includes('gpt-5.6') && m.includes('sol')) return [5.00, 30.00];
if (m.includes('gpt-5.6') && m.includes('terra')) return [2.00, 12.00];
if (m.includes('gpt-5.6') && m.includes('luna')) return [0.20, 1.20];
if (m.includes('deepseek') && m.includes('pro')) return [1.32, 3.96];
if (m.includes('deepseek') && m.includes('flash')) return [0.44, 1.32];
if (m.includes('3.8') && m.includes('flash')) return [0.75, 3.75];
if (m.includes('3.7') && m.includes('flash')) return [1.50, 7.50];
if (m.includes('3.6') && m.includes('flash')) return [1.50, 7.50];
if (m.includes('3.5') && m.includes('flash-lite')) return [0.30, 2.50];
if (m.includes('3.5') && m.includes('flash')) return [1.50, 9.00];
if (m.includes('3.1') && m.includes('flash-lite')) return [0.25, 1.50];
if (m.includes('3.1') && m.includes('pro')) return [2.00, 12.00];
if (m.includes('3') && m.includes('flash')) return [0.50, 3.00];
if (m.includes('2.5') && m.includes('flash-lite')) return [0.10, 0.40];
if (m.includes('2.5') && m.includes('flash')) return [0.30, 2.50];
if (m.includes('2.5') && m.includes('pro')) return [1.25, 10.00];
return [1.00, 5.00];
}
/**
* Returns estimated base prompt, reply, and thought token counts for a verbosity and thinking level.
* @param {string} verbosity - minimal, standard, thorough, detailed/verbose.
* @param {string|null} thinkingLevel - Thinking mode/level string.
* @returns {{inputTokens: number, replyTokens: number, thoughtTokens: number}}
*/
export function getEstimatedTokens(verbosity, thinkingLevel) {
const verb = String(verbosity || 'standard').toLowerCase();
let inputTokens = 400;
let replyTokens = 500;
if (verb === 'minimal') {
inputTokens = 250;
replyTokens = 150;
} else if (verb === 'standard') {
inputTokens = 400;
replyTokens = 500;
} else if (verb === 'thorough') {
inputTokens = 600;
replyTokens = 1200;
} else if (verb === 'detailed' || verb === 'verbose') {
inputTokens = 800;
replyTokens = 2500;
}
const normLevel = (thinkingLevel === null || thinkingLevel === undefined || thinkingLevel === '' || thinkingLevel === 'none' || thinkingLevel === 'off')
? 'none'
: String(thinkingLevel).toLowerCase();
let thoughtTokens = 0;
if (normLevel === 'minimal') {
thoughtTokens = verb === 'minimal' ? 250 : (verb === 'standard' ? 500 : (verb === 'thorough' ? 800 : 1200));
} else if (normLevel === 'low') {
thoughtTokens = verb === 'minimal' ? 600 : (verb === 'standard' ? 1200 : (verb === 'thorough' ? 2000 : 3000));
} else if (normLevel === 'medium') {
thoughtTokens = verb === 'minimal' ? 1500 : (verb === 'standard' ? 3500 : (verb === 'thorough' ? 5500 : 8000));
} else if (normLevel === 'high') {
thoughtTokens = verb === 'minimal' ? 4000 : (verb === 'standard' ? 8000 : (verb === 'thorough' ? 12000 : 16000));
} else if (normLevel === 'xhigh') {
thoughtTokens = verb === 'minimal' ? 7000 : (verb === 'standard' ? 14000 : (verb === 'thorough' ? 20000 : 28000));
} else if (normLevel === 'max') {
thoughtTokens = verb === 'minimal' ? 10000 : (verb === 'standard' ? 20000 : (verb === 'thorough' ? 30000 : 40000));
}
return { inputTokens, replyTokens, thoughtTokens };
}
/**
* Calculates the estimated cost for a given model, verbosity, and thinking level.
* @param {string} modelId - Model API identifier.
* @param {string} verbosity - Verbosity level.
* @param {string|null} thinkingLevel - Thinking level.
* @returns {{total: number, inputTokens: number, replyTokens: number, thoughtTokens: number}}
*/
export function calculateEstimatedCost(modelId, verbosity, thinkingLevel) {
const [rateInput, rateOutput] = getModelRates(modelId);
const { inputTokens, replyTokens, thoughtTokens } = getEstimatedTokens(verbosity, thinkingLevel);
const promptCost = (inputTokens / 1000000) * rateInput * 1.3;
const replyCost = (replyTokens / 1000000) * rateOutput * 1.1;
const thoughtCost = (thoughtTokens / 1000000) * rateOutput * 1.2;
const total = promptCost + replyCost + thoughtCost;
return { total, inputTokens, replyTokens, thoughtTokens };
}
/**
* Formats a currency cost value nicely.
* @param {number} cost - The cost in dollars.
* @returns {string} Formatted string (e.g., "$0.00032").
*/
export function formatEstimatedCost(cost) {
if (cost <= 0) return '$0.00000';
if (cost < 0.00001) return '<$0.00001';
if (cost < 0.001) return `$${cost.toFixed(5)}`;
if (cost < 0.01) return `$${cost.toFixed(4)}`;
return `$${cost.toFixed(4)}`;
}
/**
* Generates an HTML table of estimated costs across the 4 verbosities.
* @param {string} modelId - Selected model ID.
* @param {string|null} thinkingLevel - Selected thinking level.
* @returns {string} HTML table string.
*/
export function renderModelCostTableHtml(modelId, thinkingLevel) {
const verbosities = [
{ key: 'minimal', label: 'Minimal' },
{ key: 'standard', label: 'Standard' },
{ key: 'thorough', label: 'Thorough' },
{ key: 'detailed', label: 'Verbose' }
];
const rows = verbosities.map(v => {
const res = calculateEstimatedCost(modelId, v.key, thinkingLevel);
const formattedCost = formatEstimatedCost(res.total);
let tokenSummary = `${res.inputTokens} in / ${res.replyTokens} out`;
if (res.thoughtTokens > 0) {
tokenSummary += ` + ${res.thoughtTokens >= 1000 ? (res.thoughtTokens / 1000).toFixed(1) + 'k' : res.thoughtTokens} think`;
}
return `<tr>
<td><strong>${v.label}</strong></td>
<td style="color: [FG-muted]; font-size: 0.9em;">${tokenSummary}</td>
<td class="cost-val"><strong>${formattedCost}</strong></td>
</tr>`;
}).join('');
return `<table class="table-model-costs">
<thead>
<tr>
<th>Verbosity</th>
<th>Tokens (Est.)</th>
<th style="text-align: right;">Est. Cost</th>
</tr>
</thead>
<tbody>
${rows}
</tbody>
</table>`;
}
/**
* Updates the description and cost table DOM elements for a selected model and thinking level.
* @param {HTMLElement|null} descEl - Description container.
* @param {HTMLElement|null} costEl - Cost table container.
* @param {string} familyKey - Selected model family.
* @param {string} modelId - Selected model version.
* @param {string|null} thinkingLevel - Selected thinking level.
* @param {object} familiesData - Families definitions map.
*/
export function updateModelDetailsDisplay(descEl, costEl, familyKey, modelId, thinkingLevel, familiesData) {
const familyObj = familiesData?.[familyKey];
const modelObj = familyObj?.models?.find(m => m.model === modelId);
if (descEl) {
const desc = modelObj?.description || '';
descEl.textContent = desc;
descEl.style.display = desc ? 'block' : 'none';
}
if (costEl) {
if (modelId) {
costEl.innerHTML = renderModelCostTableHtml(modelId, thinkingLevel);
costEl.style.display = 'block';
} else {
costEl.innerHTML = '';
costEl.style.display = 'none';
}
}
}
settings-utils.js
×
Type: Web, text/plain
15.99 Kilobytes
Last Modified 2026-09-05 05:51:31
⬇ Download File
Type: Web, text/plain
15.99 Kilobytes
Last Modified 2026-09-05 05:51:31
⬇ Download File