+
-
\ No newline at end of file
+
diff --git a/app/components/Settings/AIServiceProvider.vue b/app/components/Settings/AIServiceProvider.vue
index fcfeb88..4b367dd 100644
--- a/app/components/Settings/AIServiceProvider.vue
+++ b/app/components/Settings/AIServiceProvider.vue
@@ -178,6 +178,22 @@ const deleteModels = async () => {
await Promise.all(provider.value.models.map(m => triplit.delete('models', m.id)));
};
+const enableAllModels = async () => {
+ if (!provider.value) return;
+
+ await Promise.all(provider.value.models.map(m => triplit.update('models', m.id, {
+ enabled: true
+ })));
+};
+
+const disableAllModels = async () => {
+ if (!provider.value) return;
+
+ await Promise.all(provider.value.models.map(m => triplit.update('models', m.id, {
+ enabled: false
+ })));
+};
+
const enabledModels = computed(() =>
filterModels(provider.value?.models.filter(m => m.enabled === true) as Model[] || [], modelSearch.value)
.sort(sortByReleaseDate)
@@ -188,6 +204,164 @@ const disabledModels = computed(() =>
.sort(sortByReleaseDate)
)
+// ============================================
+// Custom Model Add/Edit Panel
+// ============================================
+
+const showAddModelPanel = ref(false);
+const editingModel = ref
(null);
+
+interface ModelFormData {
+ name: string;
+ externalId: string;
+ contextWindow: string;
+ capabilities: string[];
+ promptCost: string;
+ completionCost: string;
+ reasoning: boolean;
+ tools: boolean;
+ vision: boolean;
+}
+
+const defaultFormData: ModelFormData = {
+ name: '',
+ externalId: '',
+ contextWindow: '',
+ capabilities: [],
+ promptCost: '',
+ completionCost: '',
+ reasoning: false,
+ tools: false,
+ vision: false,
+};
+
+const formData = ref({ ...defaultFormData });
+
+const availableCapabilities = ['reasoning', 'tools', 'vision'];
+
+const toggleCapability = (cap: string) => {
+ const idx = formData.value.capabilities.indexOf(cap);
+ if (idx === -1) {
+ formData.value.capabilities.push(cap);
+ } else {
+ formData.value.capabilities.splice(idx, 1);
+ }
+
+ if (cap === 'reasoning') {
+ formData.value.reasoning = formData.value.capabilities.includes('reasoning');
+ } else if (cap === 'tools') {
+ formData.value.tools = formData.value.capabilities.includes('tools');
+ } else if (cap === 'vision') {
+ formData.value.vision = formData.value.capabilities.includes('vision');
+ }
+};
+
+const resetForm = () => {
+ formData.value = { ...defaultFormData };
+ formData.value.capabilities = [];
+ editingModel.value = null;
+};
+
+const openAddPanel = () => {
+ resetForm();
+ showAddModelPanel.value = true;
+};
+
+const openEditPanel = (model: Model) => {
+ editingModel.value = model;
+ formData.value = {
+ name: model.name || '',
+ externalId: model.externalId || '',
+ contextWindow: model.attributes?.contextWindow?.toString() || '',
+ capabilities: [...(model.attributes?.capabilities || [])],
+ promptCost: model.cost?.prompt || '',
+ completionCost: model.cost?.completion || '',
+ reasoning: model.attributes?.capabilities?.has('reasoning') || false,
+ tools: model.attributes?.capabilities?.has('tools') || false,
+ vision: model.attributes?.capabilities?.has('vision') || false,
+ };
+ showAddModelPanel.value = true;
+};
+
+const { user } = useAuth();
+
+// Preview model that reacts to form changes
+const previewModel = computed(() => {
+ if (!formData.value.name || !formData.value.externalId) return null;
+
+ const inputModalities = new Set(['text']);
+ const outputModalities = new Set(['text']);
+
+ if (formData.value.capabilities.includes('vision')) {
+ inputModalities.add('image');
+ }
+
+ return {
+ userId: user.value?.id!,
+ id: editingModel.value?.id || 'preview',
+ providerId: provider.value?.id || '',
+ name: formData.value.name,
+ externalId: formData.value.externalId || 'model/id',
+ cost: {
+ prompt: formData.value.promptCost ? formatMoney(formData.value.promptCost) : undefined,
+ completion: formData.value.completionCost ? formatMoney(formData.value.completionCost) : undefined,
+ },
+ attributes: {
+ inputModalities,
+ outputModalities,
+ capabilities: new Set(formData.value.capabilities.filter(c => c !== 'vision') as any),
+ contextWindow: formData.value.contextWindow ? parseInt(formData.value.contextWindow) : null,
+ },
+ isCustom: true,
+ enabled: true,
+ provider: provider.value!,
+ } as Model;
+});
+
+const saveCustomModel = async () => {
+ if (!provider.value || !formData.value.name || !formData.value.externalId) return;
+
+ const { id, ...previewModelWithoutId } = previewModel.value!;
+ if (editingModel.value) {
+ // Update existing model
+ await triplit.update('models', editingModel.value.id, previewModelWithoutId);
+ } else {
+ // Insert new custom model
+ await triplit.insert('models', previewModelWithoutId);
+ }
+
+ showAddModelPanel.value = false;
+ resetForm();
+};
+
+const cancelPanel = () => {
+ showAddModelPanel.value = false;
+ resetForm();
+};
+
+const formatMoney = (value: string) => {
+ // ensure that there are two decimal places MINIMUM I WILL STRANGLE YOU SO HELP ME GOD
+ let [integerPart, fractionalPart] = value.split('.');
+
+ if (fractionalPart === undefined || fractionalPart === '') {
+ fractionalPart = '00';
+ }
+
+ if (fractionalPart.length === 1) {
+ fractionalPart += '0';
+ }
+
+ if (integerPart!.length === 0) {
+ integerPart = '0';
+ }
+
+ return `${integerPart}.${fractionalPart}`;
+}
+
+const isFormValid = computed(() => {
+ return formData.value.name.trim() && formData.value.externalId.trim();
+});
+
defineEmits(['navigate']);
@@ -259,43 +433,191 @@ defineEmits(['navigate']);
fetch models
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-