+
+
+ {t('settings.ai.feasibility.title', 'Model suitability and cost preview')}
+
+
+ {t(
+ 'settings.ai.feasibility.advisory',
+ 'Based on saved metadata, not verified live prices, quotas, keys or availability. This is advisory only and does not change any workload route.'
+ )}
+
+
+
+
+ {t('settings.ai.feasibility.input', 'Estimated input tokens')}
+ setInputText(e.target.value)}
+ className="w-full rounded border border-line-subtle bg-transparent px-2 py-1 text-content"
+ />
+
+
+ {t('settings.ai.feasibility.cached', 'Cached input tokens (within input)')}
+ setCachedInputText(e.target.value)}
+ className="w-full rounded border border-line-subtle bg-transparent px-2 py-1 text-content"
+ />
+
+
+ {t('settings.ai.feasibility.output', 'Estimated output tokens')}
+ setOutputText(e.target.value)}
+ className="w-full rounded border border-line-subtle bg-transparent px-2 py-1 text-content"
+ />
+
+
+ {t('settings.ai.feasibility.budget', 'Cost limit (USD, optional)')}
+ setBudgetText(e.target.value)}
+ className="w-full rounded border border-line-subtle bg-transparent px-2 py-1 text-content"
+ />
+
+
+ setRequiresVision(e.target.checked)}
+ />
+ {t('settings.ai.feasibility.requireVision', 'Require vision')}
+
+
+ {!report ? (
+
+ {cachedExceedsInput
+ ? t(
+ 'settings.ai.feasibility.invalidCached',
+ 'Cached input must be within total input tokens.'
+ )
+ : t(
+ 'settings.ai.feasibility.invalidCounts',
+ 'Enter non-negative whole token counts (cached input is a subset) and an optional non-negative cost limit.'
+ )}
+
+ ) : (
+ <>
+
+ {t('settings.ai.feasibility.fitCount', 'Saved-metadata fit')}:{' '}
+ {report.counts.metadata_fit}; {t('settings.ai.feasibility.unknownCount', 'unknown')}:{' '}
+ {report.counts.unknown};{' '}
+ {t('settings.ai.feasibility.excludedCount', 'excluded by supplied constraints')}:{' '}
+ {report.counts.excluded}.
+
+
+
+ {t('settings.ai.feasibility.search', 'Search suitability results')}
+ {
+ setQuery(e.target.value);
+ setVisibleCount(PAGE_SIZE);
+ }}
+ className="w-full rounded border border-line-subtle bg-transparent px-2 py-1 text-content"
+ />
+
+
+ {t('settings.ai.feasibility.filter', 'Filter suitability status')}
+ {
+ setFitStatus(e.target.value as 'all' | ModelFitStatus);
+ setVisibleCount(PAGE_SIZE);
+ }}
+ className="w-full rounded border border-line-subtle bg-surface px-2 py-1 text-content">
+ {t('settings.ai.feasibility.all', 'All statuses')}
+
+ {t('settings.ai.feasibility.fits', 'Metadata fit')}
+
+
+ {t('settings.ai.feasibility.unknown', 'Unknown eligibility')}
+
+
+ {t('settings.ai.feasibility.excluded', 'Excluded')}
+
+
+
+
+
+ {displayed.map(({ model, estimatedCostUSD, reasons, status }) => (
+
+
+
+ {model.provider}/{model.id}
+
+
+ {status === 'metadata_fit'
+ ? t(
+ 'settings.ai.feasibility.fitDescription',
+ 'Matches saved metadata; availability unverified'
+ )
+ : status === 'unknown'
+ ? t('settings.ai.feasibility.unknown', 'Unknown eligibility')
+ : t(
+ 'settings.ai.feasibility.excludedDescription',
+ 'Excluded by constraints'
+ )}
+ {reasons.length > 0 &&
+ ': ' + reasons.map(reason => t(...reasonLabels[reason])).join('; ')}
+
+
+
+ {estimatedCostUSD === null
+ ? t('settings.ai.feasibility.costUnknown', 'Cost unknown')
+ : formatEstimatedModelCost(estimatedCostUSD)}
+
+
+ ))}
+ {matching.length === 0 && (
+
+ {report.rows.length === 0
+ ? t('settings.ai.feasibility.empty', 'No saved models to evaluate.')
+ : t('settings.ai.feasibility.noMatch', 'No models match these filters.')}
+
+ )}
+
+
+ {t('settings.ai.feasibility.showing', 'Showing')} {displayed.length}{' '}
+ {t('settings.ai.feasibility.of', 'of')} {matching.length}{' '}
+ {t('settings.ai.feasibility.matching', 'matching model entries')}.
+
+ {displayed.length < matching.length && (
+ setVisibleCount(count => Math.min(matching.length, count + PAGE_SIZE))}
+ className="rounded-md border border-line px-3 py-1.5 text-xs text-content hover:bg-surface-subtle">
+ {t('settings.ai.feasibility.more', 'Show more models')}
+
+ )}
+ >
+ )}
+
+ );
+}
diff --git a/app/src/components/settings/panels/ai/modelFeasibility.test.ts b/app/src/components/settings/panels/ai/modelFeasibility.test.ts
new file mode 100644
index 00000000000..4ae037acdfa
--- /dev/null
+++ b/app/src/components/settings/panels/ai/modelFeasibility.test.ts
@@ -0,0 +1,227 @@
+import { describe, expect, it } from 'vitest';
+
+import {
+ assessModelFeasibility,
+ type FeasibilityModel,
+ formatEstimatedModelCost,
+ type ModelRequirements,
+} from './modelFeasibility';
+
+const model = (id: string, fields: Partial