feat(core-api): add rapid order transcribe and parse endpoints

This commit is contained in:
zouantchaw
2026-02-27 11:12:32 -05:00
parent feb38c81fa
commit 7740ad4d2d
7 changed files with 808 additions and 12 deletions

View File

@@ -6,9 +6,12 @@ import { requireAuth, requirePolicy } from '../middleware/auth.js';
import { createSignedUrlSchema } from '../contracts/core/create-signed-url.js';
import { createVerificationSchema } from '../contracts/core/create-verification.js';
import { invokeLlmSchema } from '../contracts/core/invoke-llm.js';
import { rapidOrderParseSchema } from '../contracts/core/rapid-order-parse.js';
import { rapidOrderTranscribeSchema } from '../contracts/core/rapid-order-transcribe.js';
import { reviewVerificationSchema } from '../contracts/core/review-verification.js';
import { invokeVertexModel } from '../services/llm.js';
import { checkLlmRateLimit } from '../services/llm-rate-limit.js';
import { parseRapidOrderText, transcribeRapidOrderAudio } from '../services/rapid-order.js';
import {
ensureFileExistsForActor,
generateReadSignedUrl,
@@ -24,7 +27,22 @@ import {
const DEFAULT_MAX_FILE_BYTES = 10 * 1024 * 1024;
const DEFAULT_MAX_SIGNED_URL_SECONDS = 900;
const ALLOWED_FILE_TYPES = new Set(['application/pdf', 'image/jpeg', 'image/jpg', 'image/png']);
const ALLOWED_FILE_TYPES = new Set([
'application/pdf',
'image/jpeg',
'image/jpg',
'image/png',
'audio/webm',
'audio/wav',
'audio/x-wav',
'audio/mpeg',
'audio/mp3',
'audio/mp4',
'audio/m4a',
'audio/aac',
'audio/ogg',
'audio/flac',
]);
const upload = multer({
storage: multer.memoryStorage(),
@@ -59,6 +77,10 @@ function requireVerificationFileExists() {
return process.env.VERIFICATION_REQUIRE_FILE_EXISTS !== 'false';
}
function requireRapidAudioFileExists() {
return process.env.RAPID_AUDIO_REQUIRE_FILE_EXISTS !== 'false';
}
function parseBody(schema, body) {
const parsed = schema.safeParse(body);
if (!parsed.success) {
@@ -69,6 +91,15 @@ function parseBody(schema, body) {
return parsed.data;
}
function enforceLlmRateLimit(uid) {
const rateLimit = checkLlmRateLimit({ uid });
if (!rateLimit.allowed) {
throw new AppError('RATE_LIMITED', 'Too many model requests. Please retry shortly.', 429, {
retryAfterSeconds: rateLimit.retryAfterSeconds,
});
}
}
async function handleUploadFile(req, res, next) {
try {
const file = req.file;
@@ -158,12 +189,7 @@ async function handleCreateSignedUrl(req, res, next) {
async function handleInvokeLlm(req, res, next) {
try {
const payload = parseBody(invokeLlmSchema, req.body || {});
const rateLimit = checkLlmRateLimit({ uid: req.actor.uid });
if (!rateLimit.allowed) {
throw new AppError('RATE_LIMITED', 'Too many model requests. Please retry shortly.', 429, {
retryAfterSeconds: rateLimit.retryAfterSeconds,
});
}
enforceLlmRateLimit(req.actor.uid);
const startedAt = Date.now();
if (process.env.LLM_MOCK === 'false') {
@@ -194,6 +220,63 @@ async function handleInvokeLlm(req, res, next) {
}
}
async function handleRapidOrderTranscribe(req, res, next) {
try {
const payload = parseBody(rapidOrderTranscribeSchema, req.body || {});
validateFileUriAccess({
fileUri: payload.audioFileUri,
actorUid: req.actor.uid,
});
if (requireRapidAudioFileExists() && !useMockUpload()) {
await ensureFileExistsForActor({
fileUri: payload.audioFileUri,
actorUid: req.actor.uid,
});
}
enforceLlmRateLimit(req.actor.uid);
const startedAt = Date.now();
const result = await transcribeRapidOrderAudio({
audioFileUri: payload.audioFileUri,
locale: payload.locale,
promptHints: payload.promptHints,
});
return res.status(200).json({
...result,
latencyMs: Date.now() - startedAt,
requestId: req.requestId,
});
} catch (error) {
return next(error);
}
}
async function handleRapidOrderParse(req, res, next) {
try {
const payload = parseBody(rapidOrderParseSchema, req.body || {});
enforceLlmRateLimit(req.actor.uid);
const startedAt = Date.now();
const result = await parseRapidOrderText({
text: payload.text,
locale: payload.locale,
timezone: payload.timezone,
now: payload.now,
});
return res.status(200).json({
...result,
latencyMs: Date.now() - startedAt,
requestId: req.requestId,
});
} catch (error) {
return next(error);
}
}
async function handleCreateVerification(req, res, next) {
try {
const payload = parseBody(createVerificationSchema, req.body || {});
@@ -268,6 +351,8 @@ export function createCoreRouter() {
router.post('/upload-file', requireAuth, requirePolicy('core.upload', 'file'), upload.single('file'), handleUploadFile);
router.post('/create-signed-url', requireAuth, requirePolicy('core.sign-url', 'file'), handleCreateSignedUrl);
router.post('/invoke-llm', requireAuth, requirePolicy('core.invoke-llm', 'model'), handleInvokeLlm);
router.post('/rapid-orders/transcribe', requireAuth, requirePolicy('core.rapid-order.transcribe', 'model'), handleRapidOrderTranscribe);
router.post('/rapid-orders/parse', requireAuth, requirePolicy('core.rapid-order.parse', 'model'), handleRapidOrderParse);
router.post('/verifications', requireAuth, requirePolicy('core.verification.create', 'verification'), handleCreateVerification);
router.get('/verifications/:verificationId', requireAuth, requirePolicy('core.verification.read', 'verification'), handleGetVerification);
router.post('/verifications/:verificationId/review', requireAuth, requirePolicy('core.verification.review', 'verification'), handleReviewVerification);