diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 436350a..6d36f4e 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -6,8 +6,17 @@ "WebFetch(domain:github.com)", "Bash(git init:*)", "Bash(git add:*)", - "Bash(git commit:*)" - ] + "Bash(git commit:*)", + "Bash(git checkout:*)", + "Bash(DATABASE_URL=./local.db npx drizzle-kit generate 2>&1)", + "Bash(DATABASE_URL=./local.db npx drizzle-kit generate)", + "Bash(npm run:*)", + "Bash(npm test:*)", + "Skill(update-config)", + "Bash(git -C /home/moze/Sources/trueref checkout -b feat/TRUEREF-0002-through-0018)", + "Bash(git:*)" + ], + "defaultMode": "bypassPermissions" }, "enableAllProjectMcpServers": true, "enabledMcpjsonServers": [ diff --git a/src/routes/api/v1/settings/embedding/test/+server.ts b/src/routes/api/v1/settings/embedding/test/+server.ts new file mode 100644 index 0000000..5e81329 --- /dev/null +++ b/src/routes/api/v1/settings/embedding/test/+server.ts @@ -0,0 +1,115 @@ +/** + * POST /api/v1/settings/embedding/test + * + * Validates an embedding provider configuration by creating a provider + * instance and calling embed(['test']). Returns success with dimensions + * or a descriptive error without persisting any changes. + */ + +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { + createProviderFromConfig, + type EmbeddingConfig +} from '$lib/server/embeddings/factory'; +import { handleServiceError, InvalidInputError } from '$lib/server/utils/validation'; + +// --------------------------------------------------------------------------- +// Validate — reuse the same shape accepted by PUT /settings/embedding +// --------------------------------------------------------------------------- + +function validateConfig(body: unknown): EmbeddingConfig { + if (typeof body !== 'object' || body === null) { + throw new InvalidInputError('Request body must be a JSON object'); + } + + const obj = body as Record; + + const provider = obj.provider; + if (provider !== 'openai' && provider !== 'local' && provider !== 'none') { + throw new InvalidInputError( + `Invalid provider "${String(provider)}". Must be one of: openai, local, none.` + ); + } + + if (provider === 'openai') { + const openai = obj.openai as Record | undefined; + if (!openai || typeof openai !== 'object') { + throw new InvalidInputError('openai config object is required when provider is "openai"'); + } + if (typeof openai.baseUrl !== 'string' || !openai.baseUrl) { + throw new InvalidInputError('openai.baseUrl must be a non-empty string'); + } + if (typeof openai.apiKey !== 'string' || !openai.apiKey) { + throw new InvalidInputError('openai.apiKey must be a non-empty string'); + } + if (typeof openai.model !== 'string' || !openai.model) { + throw new InvalidInputError('openai.model must be a non-empty string'); + } + + return { + provider: 'openai', + openai: { + baseUrl: openai.baseUrl as string, + apiKey: openai.apiKey as string, + model: openai.model as string, + dimensions: + typeof openai.dimensions === 'number' ? (openai.dimensions as number) : undefined, + maxBatchSize: + typeof openai.maxBatchSize === 'number' ? (openai.maxBatchSize as number) : undefined + } + }; + } + + return { provider: provider as 'local' | 'none' }; +} + +// --------------------------------------------------------------------------- +// POST +// --------------------------------------------------------------------------- + +export const POST: RequestHandler = async ({ request }) => { + try { + const body = await request.json(); + const config = validateConfig(body); + + if (config.provider === 'none') { + throw new InvalidInputError('Cannot test the "none" provider — no backend is configured.'); + } + + const provider = createProviderFromConfig(config); + + const available = await provider.isAvailable(); + if (!available) { + return new Response( + JSON.stringify({ + error: `Provider "${config.provider}" is not available. Check your configuration.` + }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + + // Perform a real embedding call to catch auth / model-name errors. + let dimensions: number; + try { + const results = await provider.embed(['test']); + if (results.length === 0) { + return new Response( + JSON.stringify({ error: 'Provider returned no embeddings for the test input.' }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + dimensions = results[0].dimensions; + } catch (embedErr) { + const message = embedErr instanceof Error ? embedErr.message : 'Embedding call failed'; + return new Response(JSON.stringify({ error: message }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }); + } + + return json({ ok: true, dimensions }); + } catch (err) { + return handleServiceError(err); + } +};