mirror of
https://github.com/JohnRiceML/clawport-ui.git
synced 2026-08-14 00:47:50 +00:00
Images attached by users are now sent to the agent as OpenAI vision content parts so the model can actually see them. Text files are inlined, binary files described. Validation updated to accept multimodal content arrays. Added /api/tts endpoint (OpenAI TTS via gateway) and a speaker button on assistant messages for on-demand text-to-speech playback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
81 lines
2.5 KiB
TypeScript
81 lines
2.5 KiB
TypeScript
// Validation for chat API messages — supports both plain text and multimodal (vision) content
|
|
|
|
export type TextContentPart = { type: 'text'; text: string }
|
|
export type ImageContentPart = { type: 'image_url'; image_url: { url: string } }
|
|
export type ContentPart = TextContentPart | ImageContentPart
|
|
|
|
export type MessageContent = string | ContentPart[]
|
|
|
|
export interface ApiMessage {
|
|
role: 'user' | 'assistant' | 'system'
|
|
content: MessageContent
|
|
}
|
|
|
|
function isValidContentPart(part: unknown): part is ContentPart {
|
|
if (typeof part !== 'object' || part === null) return false
|
|
const p = part as Record<string, unknown>
|
|
|
|
if (p.type === 'text') {
|
|
return typeof p.text === 'string'
|
|
}
|
|
|
|
if (p.type === 'image_url') {
|
|
const imgUrl = p.image_url
|
|
if (typeof imgUrl !== 'object' || imgUrl === null) return false
|
|
return typeof (imgUrl as Record<string, unknown>).url === 'string'
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
function isValidContent(content: unknown): content is MessageContent {
|
|
if (typeof content === 'string') return true
|
|
|
|
if (Array.isArray(content)) {
|
|
return content.length > 0 && content.every(isValidContentPart)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
export function validateMessages(messages: unknown): ApiMessage[] {
|
|
if (!Array.isArray(messages)) {
|
|
throw new Error('messages must be an array')
|
|
}
|
|
|
|
return messages.map((msg, i) => {
|
|
if (typeof msg !== 'object' || msg === null) {
|
|
throw new Error(`messages[${i}] must be an object`)
|
|
}
|
|
|
|
const m = msg as Record<string, unknown>
|
|
|
|
if (typeof m.role !== 'string' || !['user', 'assistant', 'system'].includes(m.role)) {
|
|
throw new Error(`messages[${i}].role must be "user", "assistant", or "system"`)
|
|
}
|
|
|
|
if (!isValidContent(m.content)) {
|
|
throw new Error(`messages[${i}].content must be a string or array of content parts`)
|
|
}
|
|
|
|
return { role: m.role as ApiMessage['role'], content: m.content as MessageContent }
|
|
})
|
|
}
|
|
|
|
// Legacy export for backward compatibility with existing test
|
|
export type ValidatedChatMessage = ApiMessage
|
|
export type ValidationResult = { ok: true; messages: ApiMessage[] } | { ok: false; error: string }
|
|
|
|
export function validateChatMessages(body: unknown): ValidationResult {
|
|
if (body === null || typeof body !== 'object') {
|
|
return { ok: false, error: 'Request body must be a JSON object.' }
|
|
}
|
|
const { messages } = body as Record<string, unknown>
|
|
try {
|
|
const validated = validateMessages(messages)
|
|
return { ok: true, messages: validated }
|
|
} catch (err) {
|
|
return { ok: false, error: err instanceof Error ? err.message : 'Invalid messages' }
|
|
}
|
|
}
|