feat: integration of models.dev and proper handling of outpout tokens

This commit is contained in:
2026-03-01 14:04:23 +01:00
parent 6891613943
commit 1dd520f770
18 changed files with 2101 additions and 14 deletions

View File

@@ -242,6 +242,18 @@
flex: 1;
}
.setting-input-group select {
flex: 1;
}
/* Model catalog metadata line */
.model-catalog-info {
font-size: 11px;
color: var(--vscode-descriptionForeground, #888);
padding: 4px 0 0;
line-height: 1.4;
}
.setting-toggle-visibility {
background: transparent;
border: none;

View File

@@ -244,6 +244,13 @@ export const SettingsView: React.FC = () => {
const [newApiKey, setNewApiKey] = useState('');
const [availableModels, setAvailableModels] = useState<{id: string; name: string}[]>([]);
const [selectedModel, setSelectedModel] = useState('');
const [modelCatalog, setModelCatalog] = useState<Map<string, {
maxOutputTokens: number | null;
contextWindow: number | null;
inputPrice: number | null;
outputPrice: number | null;
}>>(new Map());
const [refreshingCatalog, setRefreshingCatalog] = useState(false);
// Check if a section has any matching settings
const sectionHasMatches = useCallback((sectionKeywords: string[]) => {
@@ -395,6 +402,21 @@ export const SettingsView: React.FC = () => {
setAvailableModels(modelsResult.models);
setSelectedModel(modelsResult.selectedModel || '');
}
// Load model catalog metadata
const catalogResult = await window.electronAPI?.chat.getModelCatalog();
if (catalogResult?.success && catalogResult.entries) {
const map = new Map<string, { maxOutputTokens: number | null; contextWindow: number | null; inputPrice: number | null; outputPrice: number | null }>();
for (const entry of catalogResult.entries) {
map.set(entry.id, {
maxOutputTokens: entry.maxOutputTokens,
contextWindow: entry.contextWindow,
inputPrice: entry.inputPrice,
outputPrice: entry.outputPrice,
});
}
setModelCatalog(map);
}
} catch (error) {
console.error('Failed to load AI settings:', error);
}
@@ -1080,6 +1102,41 @@ export const SettingsView: React.FC = () => {
}
};
const handleRefreshModelCatalog = async () => {
setRefreshingCatalog(true);
try {
const result = await window.electronAPI?.chat.refreshModelCatalog();
if (result?.success) {
if (result.notModified) {
showToast.success(t('settings.toast.modelCatalogUpToDate'));
} else {
showToast.success(t('settings.toast.modelCatalogRefreshed', { count: String(result.modelsUpdated) }));
}
// Reload catalog data
const catalogResult = await window.electronAPI?.chat.getModelCatalog();
if (catalogResult?.success && catalogResult.entries) {
const map = new Map<string, { maxOutputTokens: number | null; contextWindow: number | null; inputPrice: number | null; outputPrice: number | null }>();
for (const entry of catalogResult.entries) {
map.set(entry.id, {
maxOutputTokens: entry.maxOutputTokens,
contextWindow: entry.contextWindow,
inputPrice: entry.inputPrice,
outputPrice: entry.outputPrice,
});
}
setModelCatalog(map);
}
} else {
showToast.error(t('settings.toast.modelCatalogRefreshFailed'));
}
} catch (error) {
console.error('Failed to refresh model catalog:', error);
showToast.error(t('settings.toast.modelCatalogRefreshFailed'));
} finally {
setRefreshingCatalog(false);
}
};
const renderAISettings = () => (
<SettingSection
id="settings-section-ai"
@@ -1133,17 +1190,46 @@ export const SettingsView: React.FC = () => {
label={t('settings.ai.defaultModelLabel')}
description={t('settings.ai.defaultModelDescription')}
>
<select
id="ai-model"
value={selectedModel}
onChange={(e) => handleModelChange(e.target.value)}
disabled={!aiHasApiKey}
>
{availableModels.length === 0 && <option value="">{t('settings.ai.noModels')}</option>}
{availableModels.map(model => (
<option key={model.id} value={model.id}>{model.name}</option>
))}
</select>
<div className="setting-input-group">
<select
id="ai-model"
value={selectedModel}
onChange={(e) => handleModelChange(e.target.value)}
disabled={!aiHasApiKey}
>
{availableModels.length === 0 && <option value="">{t('settings.ai.noModels')}</option>}
{availableModels.map(model => (
<option key={model.id} value={model.id}>{model.name}</option>
))}
</select>
<button
className="secondary"
onClick={handleRefreshModelCatalog}
disabled={refreshingCatalog || !aiHasApiKey}
title={t('settings.ai.refreshModelCatalog')}
>
{refreshingCatalog ? t('settings.ai.refreshing') : t('settings.ai.refreshModelCatalog')}
</button>
</div>
{selectedModel && modelCatalog.has(selectedModel) && (() => {
const info = modelCatalog.get(selectedModel)!;
const parts: string[] = [];
if (info.maxOutputTokens != null) {
parts.push(`${t('settings.ai.modelInfoMaxOutput')}: ${info.maxOutputTokens.toLocaleString()} ${t('settings.ai.modelInfoTokens')}`);
}
if (info.contextWindow != null) {
parts.push(`${t('settings.ai.modelInfoContext')}: ${info.contextWindow.toLocaleString()} ${t('settings.ai.modelInfoTokens')}`);
}
if (info.inputPrice != null) {
parts.push(`${t('settings.ai.modelInfoInputPrice')}: $${info.inputPrice}${t('settings.ai.modelInfoPerMTok')}`);
}
if (info.outputPrice != null) {
parts.push(`${t('settings.ai.modelInfoOutputPrice')}: $${info.outputPrice}${t('settings.ai.modelInfoPerMTok')}`);
}
return parts.length > 0 ? (
<div className="model-catalog-info">{parts.join(' · ')}</div>
) : null;
})()}
</SettingRow>
<SettingRow

View File

@@ -725,6 +725,17 @@
"settings.ai.systemPromptPlaceholder": "Systemanweisungen für den KI-Assistenten eingeben...",
"settings.ai.savePrompt": "Prompt speichern",
"settings.ai.resetPrompt": "Auf Standard zurücksetzen",
"settings.ai.refreshModelCatalog": "Modellkatalog aktualisieren",
"settings.ai.refreshing": "Wird aktualisiert…",
"settings.ai.modelInfoMaxOutput": "Max. Ausgabe",
"settings.ai.modelInfoContext": "Kontext",
"settings.ai.modelInfoInputPrice": "Eingabe",
"settings.ai.modelInfoOutputPrice": "Ausgabe",
"settings.ai.modelInfoTokens": "Token",
"settings.ai.modelInfoPerMTok": "/MTok",
"settings.toast.modelCatalogRefreshed": "Modellkatalog aktualisiert ({{count}} Modelle)",
"settings.toast.modelCatalogUpToDate": "Modellkatalog ist bereits aktuell",
"settings.toast.modelCatalogRefreshFailed": "Modellkatalog konnte nicht aktualisiert werden",
"settings.publishing.sshHostDescription": "Hostname oder IP-Adresse des SSH-Servers.",
"settings.publishing.sshUsernameDescription": "Benutzername deines SSH-Kontos.",
"settings.publishing.sshRemotePathDescription": "Das Zielverzeichnis auf dem Remote-Server, in das dein Blog veröffentlicht wird.",

View File

@@ -725,6 +725,17 @@
"settings.ai.systemPromptPlaceholder": "Enter system instructions for the AI assistant...",
"settings.ai.savePrompt": "Save Prompt",
"settings.ai.resetPrompt": "Reset to Default",
"settings.ai.refreshModelCatalog": "Refresh Model Catalog",
"settings.ai.refreshing": "Refreshing…",
"settings.ai.modelInfoMaxOutput": "Max output",
"settings.ai.modelInfoContext": "Context",
"settings.ai.modelInfoInputPrice": "Input",
"settings.ai.modelInfoOutputPrice": "Output",
"settings.ai.modelInfoTokens": "tokens",
"settings.ai.modelInfoPerMTok": "/MTok",
"settings.toast.modelCatalogRefreshed": "Model catalog updated ({{count}} models)",
"settings.toast.modelCatalogUpToDate": "Model catalog already up to date",
"settings.toast.modelCatalogRefreshFailed": "Failed to refresh model catalog",
"settings.publishing.sshHostDescription": "The SSH server hostname or IP address.",
"settings.publishing.sshUsernameDescription": "Your SSH account username.",
"settings.publishing.sshRemotePathDescription": "The destination directory on the remote server where your blog will be published.",

View File

@@ -725,6 +725,17 @@
"settings.ai.systemPromptPlaceholder": "Escribe aquí tu prompt del sistema…",
"settings.ai.savePrompt": "Guardar prompt",
"settings.ai.resetPrompt": "Restablecer prompt",
"settings.ai.refreshModelCatalog": "Actualizar catálogo de modelos",
"settings.ai.refreshing": "Actualizando…",
"settings.ai.modelInfoMaxOutput": "Salida máx.",
"settings.ai.modelInfoContext": "Contexto",
"settings.ai.modelInfoInputPrice": "Entrada",
"settings.ai.modelInfoOutputPrice": "Salida",
"settings.ai.modelInfoTokens": "tokens",
"settings.ai.modelInfoPerMTok": "/MTok",
"settings.toast.modelCatalogRefreshed": "Catálogo actualizado ({{count}} modelos)",
"settings.toast.modelCatalogUpToDate": "El catálogo ya está actualizado",
"settings.toast.modelCatalogRefreshFailed": "No se pudo actualizar el catálogo",
"settings.publishing.sshHostDescription": "Nombre de host o IP del servidor SSH.",
"settings.publishing.sshUsernameDescription": "Nombre de usuario de SSH.",
"settings.publishing.sshRemotePathDescription": "El directorio de destino en el servidor remoto donde se publicará tu blog.",

View File

@@ -723,6 +723,17 @@
"settings.ai.systemPromptPlaceholder": "Écrivez votre prompt système ici…",
"settings.ai.savePrompt": "Enregistrer le prompt",
"settings.ai.resetPrompt": "Réinitialiser le prompt",
"settings.ai.refreshModelCatalog": "Actualiser le catalogue",
"settings.ai.refreshing": "Actualisation…",
"settings.ai.modelInfoMaxOutput": "Sortie max.",
"settings.ai.modelInfoContext": "Contexte",
"settings.ai.modelInfoInputPrice": "Entrée",
"settings.ai.modelInfoOutputPrice": "Sortie",
"settings.ai.modelInfoTokens": "tokens",
"settings.ai.modelInfoPerMTok": "/MTok",
"settings.toast.modelCatalogRefreshed": "Catalogue mis à jour ({{count}} modèles)",
"settings.toast.modelCatalogUpToDate": "Le catalogue est déjà à jour",
"settings.toast.modelCatalogRefreshFailed": "Échec de l'actualisation du catalogue",
"settings.publishing.sshHostDescription": "Nom d'hôte ou IP du serveur SSH.",
"settings.publishing.sshUsernameDescription": "Nom d'utilisateur SSH.",
"settings.publishing.sshRemotePathDescription": "Le répertoire de destination sur le serveur distant où votre blog sera publié.",

View File

@@ -723,6 +723,17 @@
"settings.ai.systemPromptPlaceholder": "Scrivi qui il tuo prompt di sistema…",
"settings.ai.savePrompt": "Salva prompt",
"settings.ai.resetPrompt": "Reimposta prompt",
"settings.ai.refreshModelCatalog": "Aggiorna catalogo modelli",
"settings.ai.refreshing": "Aggiornamento…",
"settings.ai.modelInfoMaxOutput": "Output max.",
"settings.ai.modelInfoContext": "Contesto",
"settings.ai.modelInfoInputPrice": "Input",
"settings.ai.modelInfoOutputPrice": "Output",
"settings.ai.modelInfoTokens": "token",
"settings.ai.modelInfoPerMTok": "/MTok",
"settings.toast.modelCatalogRefreshed": "Catalogo aggiornato ({{count}} modelli)",
"settings.toast.modelCatalogUpToDate": "Il catalogo è già aggiornato",
"settings.toast.modelCatalogRefreshFailed": "Aggiornamento del catalogo non riuscito",
"settings.publishing.sshHostDescription": "Hostname o IP del server SSH.",
"settings.publishing.sshUsernameDescription": "Nome utente SSH.",
"settings.publishing.sshRemotePathDescription": "La directory di destinazione sul server remoto in cui verrà pubblicato il tuo blog.",