102 lines
3.2 KiB
TypeScript
102 lines
3.2 KiB
TypeScript
export interface GenerateMeetingDocumentRequest {
|
|
meetingId: string
|
|
templateId: string
|
|
idempotencyKey: string
|
|
title: string
|
|
model: 'claude-haiku-4-5-20251001' | 'claude-sonnet-4-6' | 'claude-opus-4-6'
|
|
}
|
|
|
|
export interface ProviderDocumentResult {
|
|
content: string
|
|
inputTokens: number | null
|
|
outputTokens: number | null
|
|
}
|
|
|
|
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
const MODELS = new Set<GenerateMeetingDocumentRequest['model']>([
|
|
'claude-haiku-4-5-20251001',
|
|
'claude-sonnet-4-6',
|
|
'claude-opus-4-6',
|
|
])
|
|
const REQUEST_KEYS = new Set([
|
|
'meetingId',
|
|
'templateId',
|
|
'idempotencyKey',
|
|
'title',
|
|
'model',
|
|
])
|
|
|
|
export class MeetingDocumentRequestError extends Error {
|
|
readonly status = 400
|
|
|
|
constructor(message: string) {
|
|
super(message)
|
|
this.name = 'MeetingDocumentRequestError'
|
|
}
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
}
|
|
|
|
function requireUuid(value: unknown, field: string): string {
|
|
if (typeof value !== 'string' || !UUID_PATTERN.test(value)) {
|
|
throw new MeetingDocumentRequestError(`${field} must be a UUID`)
|
|
}
|
|
return value.toLowerCase()
|
|
}
|
|
|
|
export function parseGenerateMeetingDocumentRequest(
|
|
value: unknown,
|
|
): GenerateMeetingDocumentRequest {
|
|
if (!isRecord(value)) throw new MeetingDocumentRequestError('Invalid JSON body')
|
|
const unsupported = Object.keys(value).filter((key) => !REQUEST_KEYS.has(key))
|
|
if (unsupported.length > 0) {
|
|
throw new MeetingDocumentRequestError('Unsupported request fields')
|
|
}
|
|
|
|
const title = typeof value.title === 'string'
|
|
? value.title.trim().replace(/\s+/g, ' ')
|
|
: ''
|
|
if (title.length < 1 || title.length > 160) {
|
|
throw new MeetingDocumentRequestError('title must contain 1-160 characters')
|
|
}
|
|
|
|
const model = value.model === undefined
|
|
? 'claude-haiku-4-5-20251001'
|
|
: value.model
|
|
if (typeof model !== 'string' || !MODELS.has(model as GenerateMeetingDocumentRequest['model'])) {
|
|
throw new MeetingDocumentRequestError('Unsupported model')
|
|
}
|
|
|
|
return {
|
|
meetingId: requireUuid(value.meetingId, 'meetingId'),
|
|
templateId: requireUuid(value.templateId, 'templateId'),
|
|
idempotencyKey: requireUuid(value.idempotencyKey, 'idempotencyKey'),
|
|
title,
|
|
model: model as GenerateMeetingDocumentRequest['model'],
|
|
}
|
|
}
|
|
|
|
function safeTokenCount(value: unknown): number | null {
|
|
return Number.isSafeInteger(value) && Number(value) >= 0 ? Number(value) : null
|
|
}
|
|
|
|
export function parseProviderDocumentResult(value: unknown): ProviderDocumentResult | null {
|
|
if (!isRecord(value) || !Array.isArray(value.content)) return null
|
|
const text = value.content
|
|
.filter((block): block is Record<string, unknown> => isRecord(block))
|
|
.filter((block) => block.type === 'text' && typeof block.text === 'string')
|
|
.map((block) => (block.text as string).trim())
|
|
.filter((part) => part.length > 0)
|
|
.join('\n\n')
|
|
.trim()
|
|
if (text.length < 1 || text.length > 100000) return null
|
|
|
|
const usage = isRecord(value.usage) ? value.usage : {}
|
|
return {
|
|
content: text,
|
|
inputTokens: safeTokenCount(usage.input_tokens),
|
|
outputTokens: safeTokenCount(usage.output_tokens),
|
|
}
|
|
}
|