112 lines
3.8 KiB
TypeScript
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 }
|
|
);
|
|
}
|
|
};
|