From cd8c82274272d2a6dc0c769a48a734e502b33b26 Mon Sep 17 00:00:00 2001 From: Florian Chab Date: Mon, 22 Jun 2026 14:01:46 +0200 Subject: [PATCH 1/2] feat: add Mistral AI provider support - Add Mistral to provider.ts (import, type, registry, createLanguageModel) - Add Mistral to scoring.ts schemas (createAiConfigSchema, updateAiConfigSchema) - Add @ai-sdk/mistral dependency Signed-off-by: Florian Chab --- package-lock.json | 34 +++++++++++++++++++++++++++++++++ package.json | 1 + server/utils/ai/provider.ts | 25 ++++++++++++++++++++++-- server/utils/schemas/scoring.ts | 4 ++-- 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 280e1610..b643ed36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@ai-sdk/anthropic": "^3.0.74", "@ai-sdk/google": "^3.0.67", + "@ai-sdk/mistral": "^3.0.40", "@ai-sdk/openai": "^3.0.58", "@aws-sdk/client-s3": "^3.1041.0", "@better-auth/sso": "^1.6.16", @@ -106,6 +107,39 @@ "zod": "^3.25.76 || ^4.1.8" } }, + "node_modules/@ai-sdk/mistral": { + "version": "3.0.40", + "resolved": "https://registry.npmjs.org/@ai-sdk/mistral/-/mistral-3.0.40.tgz", + "integrity": "sha512-HzCV9jFsb04kpL/N+G7SCjFKJA0Q33p4Hc+1quYGGD8Ta37Pe5bzk9AjxaS8OYd//jmcny16yKhJ+e+h+P0AJg==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.10", + "@ai-sdk/provider-utils": "4.0.30" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/mistral/node_modules/@ai-sdk/provider-utils": { + "version": "4.0.30", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.30.tgz", + "integrity": "sha512-VO7I+vPffqI5sMnPoUq5DCSqKIgQIk/naJWRdQVpz2ma2zoprC/lqiJiUEl2s6DfvTD76TbhD3q39ROjlA6rGw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.10", + "@standard-schema/spec": "^1.1.0", + "eventsource-parser": "^3.0.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, "node_modules/@ai-sdk/openai": { "version": "3.0.69", "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-3.0.69.tgz", diff --git a/package.json b/package.json index 6f3635bc..e5b3c2d4 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "dependencies": { "@ai-sdk/anthropic": "^3.0.74", "@ai-sdk/google": "^3.0.67", + "@ai-sdk/mistral": "^3.0.40", "@ai-sdk/openai": "^3.0.58", "@aws-sdk/client-s3": "^3.1041.0", "@better-auth/sso": "^1.6.16", diff --git a/server/utils/ai/provider.ts b/server/utils/ai/provider.ts index 0f385cfa..d37f3aae 100644 --- a/server/utils/ai/provider.ts +++ b/server/utils/ai/provider.ts @@ -1,18 +1,19 @@ /** * AI Provider Abstraction Layer * - * Supports OpenAI, Anthropic, and custom OpenAI-compatible endpoints. + * Supports OpenAI, Anthropic, Google, Mistral, and custom OpenAI-compatible endpoints. * Credentials are decrypted per-request from the organization's AI config. * Never logs or stores raw API keys — only encrypted values in the database. */ import { createOpenAI } from '@ai-sdk/openai' import { createAnthropic } from '@ai-sdk/anthropic' import { createGoogleGenerativeAI } from '@ai-sdk/google' +import { createMistral } from '@ai-sdk/mistral' import { generateObject } from 'ai' import type { z } from 'zod' import { decrypt } from '../encryption' -export type SupportedProvider = 'openai' | 'anthropic' | 'google' | 'openai_compatible' +export type SupportedProvider = 'openai' | 'anthropic' | 'google' | 'mistral' | 'openai_compatible' export interface ProviderConfig { provider: SupportedProvider @@ -99,6 +100,20 @@ export const PROVIDER_REGISTRY: Record Date: Mon, 22 Jun 2026 14:46:47 +0200 Subject: [PATCH 2/2] fix: allow optional API key for custom OpenAI-compatible endpoints - Update createAiConfigSchema and updateAiConfigSchema to accept empty API key (min:0) - Modify createLanguageModel to handle empty API key for openai_compatible provider - Update AiConfigForm validation to not require API key for custom endpoints - Ensure API key is always included in request body (even if empty string) This enables local endpoints (Ollama, LM Studio, vLLM, etc.) to work without requiring an API key, while maintaining security for cloud providers. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe Signed-off-by: Florian Chab --- app/components/AiConfigForm.vue | 5 +++-- server/utils/ai/provider.ts | 5 +++-- server/utils/schemas/scoring.ts | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/components/AiConfigForm.vue b/app/components/AiConfigForm.vue index 54d826f9..6b0730e3 100644 --- a/app/components/AiConfigForm.vue +++ b/app/components/AiConfigForm.vue @@ -130,7 +130,8 @@ function pickProvider(key: string) { const canSave = computed(() => { if (!form.value.name.trim()) return false if (!form.value.model.trim()) return false - if (!isEdit.value && !form.value.apiKey) return false + // API key required for cloud providers, but optional for custom endpoints (Ollama, etc.) + if (!isEdit.value && !form.value.apiKey && !isCustomProvider.value) return false if (isCustomProvider.value && !form.value.baseUrl) return false return true }) @@ -148,7 +149,7 @@ async function handleSave() { outputPricePer1m: form.value.outputPricePer1m, } if (isCustomProvider.value) body.baseUrl = form.value.baseUrl - if (form.value.apiKey) body.apiKey = form.value.apiKey + if (form.value.apiKey !== undefined) body.apiKey = form.value.apiKey if (isEdit.value && props.config) { await $fetch(`/api/ai-config/${props.config.id}`, { diff --git a/server/utils/ai/provider.ts b/server/utils/ai/provider.ts index d37f3aae..43352175 100644 --- a/server/utils/ai/provider.ts +++ b/server/utils/ai/provider.ts @@ -133,7 +133,8 @@ export function createLanguageModel(config: ProviderConfig) { const secret = env.BETTER_AUTH_SECRET const apiKey = decrypt(config.apiKeyEncrypted, secret) - if (!apiKey) { + // For openai_compatible, allow empty API key (local endpoints like Ollama don't need auth) + if (!apiKey && config.provider !== 'openai_compatible') { throw createError({ statusCode: 500, statusMessage: 'Failed to decrypt AI API key. The key may be corrupted.', @@ -144,7 +145,7 @@ export function createLanguageModel(config: ProviderConfig) { case 'openai': case 'openai_compatible': { const openai = createOpenAI({ - apiKey, + apiKey: apiKey || 'dummy-key', ...(config.baseUrl ? { baseURL: config.baseUrl } : {}), }) return openai(config.model) diff --git a/server/utils/schemas/scoring.ts b/server/utils/schemas/scoring.ts index 72970285..a09b4955 100644 --- a/server/utils/schemas/scoring.ts +++ b/server/utils/schemas/scoring.ts @@ -17,7 +17,7 @@ export const createAiConfigSchema = z.object({ name: z.string().min(1).max(80).trim(), provider: z.enum(['openai', 'anthropic', 'google', 'mistral', 'openai_compatible']), model: z.string().min(1).max(200), - apiKey: z.string().min(1).max(500), + apiKey: z.string().min(0).max(500), baseUrl: safeBaseUrl.nullish(), // Modern frontier models support 100K+ output tokens. Cap generously to avoid blocking power users. maxTokens: z.number().int().min(256).max(200000).optional().default(16384), @@ -31,7 +31,7 @@ export const updateAiConfigSchema = z.object({ name: z.string().min(1).max(80).trim().optional(), provider: z.enum(['openai', 'anthropic', 'google', 'mistral', 'openai_compatible']).optional(), model: z.string().min(1).max(200).optional(), - apiKey: z.string().min(1).max(500).optional(), + apiKey: z.string().min(0).max(500).optional(), baseUrl: safeBaseUrl.nullish(), maxTokens: z.number().int().min(256).max(200000).optional(), inputPricePer1m: z.number().min(0).max(9999).nullish(),