Files
shd.build/src/routes/api/isac/mechanic-assistant/+server.ts
T

112 lines
3.8 KiB
TypeScript

import { getSessionUser } from '$lib/server/auth/session';
import { requirePermission } from '$lib/server/auth/authorization';
import { recordIsacAudit } from '$lib/server/audit';
import {
AiMechanicConfigurationError,
AiMechanicGenerationError,
generateAiMechanicDraft
} from '$lib/server/ai/mechanic-authoring-service';
import { AI_MECHANIC_PROMPT_VERSION } from '$lib/server/ai/mechanic-authoring-prompt';
import {
AiMechanicRequestError,
parseAiMechanicAuthoringRequest
} from '$lib/server/ai/mechanic-authoring-schema';
import {
AiMechanicRateLimitError,
reserveAiMechanicGeneration
} from '$lib/server/ai/mechanic-authoring-rate-limit';
import { isAllowedShdBuildRequestOrigin } from '$lib/server/request-origin';
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
const MAX_REQUEST_BYTES = 100_000;
export const POST: RequestHandler = async ({ cookies, request, url }) => {
const operator = await requirePermission(
await getSessionUser(cookies),
'ai.mechanic.generate'
);
const origin = request.headers.get('origin');
if (!isAllowedShdBuildRequestOrigin(origin, url.origin)) {
return json(
{ message: 'Cross-origin mechanic authoring requests are not allowed.' },
{ status: 403 }
);
}
const contentLength = Number(request.headers.get('content-length') ?? 0);
if (Number.isFinite(contentLength) && contentLength > MAX_REQUEST_BYTES) {
return json({ message: 'The mechanic authoring request is too large.' }, { status: 413 });
}
let payload: unknown;
try {
const body = await request.text();
if (Buffer.byteLength(body, 'utf8') > MAX_REQUEST_BYTES) {
return json({ message: 'The mechanic authoring request is too large.' }, { status: 413 });
}
payload = JSON.parse(body);
} catch {
return json({ message: 'The mechanic authoring request must be valid JSON.' }, { status: 400 });
}
try {
const authoringRequest = parseAiMechanicAuthoringRequest(payload);
const release = reserveAiMechanicGeneration(operator.id);
let response;
try {
response = await generateAiMechanicDraft(authoringRequest, operator.id);
} finally {
release();
}
await recordIsacAudit({
actorId: operator.id,
category: 'ai_assistance',
action: 'mechanic_ai.generated',
entityId: `${authoringRequest.context.sourceKind}:${response.meta.requestId}`,
after: {
sourceKind: authoringRequest.context.sourceKind,
promptVersion: AI_MECHANIC_PROMPT_VERSION,
responseStatus: response.status,
model: response.meta.model,
requestId: response.meta.requestId,
usage: {
inputTokens: response.meta.inputTokens,
outputTokens: response.meta.outputTokens,
totalTokens: response.meta.totalTokens
},
variantKeys: authoringRequest.valueColumns.map((column) => column.variantKey),
clarificationCount: response.status === 'clarification_required'
? response.questions.length
: 0,
capabilityGapCount: response.status === 'capability_gap'
? response.gaps.length
: 0,
draftValidated: response.status === 'draft_ready'
}
});
return json(response);
} catch (caught) {
if (caught instanceof AiMechanicRequestError) {
return json({ message: caught.message }, { status: caught.status });
}
if (caught instanceof AiMechanicConfigurationError) {
return json({ message: caught.message }, { status: 503 });
}
if (caught instanceof AiMechanicRateLimitError) {
return json(
{ message: caught.message },
{
status: 429,
headers: { 'retry-after': String(caught.retryAfterSeconds) }
}
);
}
if (caught instanceof AiMechanicGenerationError) {
return json({ message: caught.message }, { status: 422 });
}
console.error('Mechanic authoring assistant failed.', caught);
return json(
{ message: 'The AI mechanic authoring service is temporarily unavailable.' },
{ status: 502 }
);
}
};