0 directories, 7 files

util

Home / testing / ai / tinai / util
/**
 * 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