Narrow the parsed JSON body without casting (project rule: no `as`).
(value: unknown)
| 42 | |
| 43 | /** Narrow the parsed JSON body without casting (project rule: no `as`). */ |
| 44 | function parseCreateRunBody(value: unknown): CreateRunBody { |
| 45 | if (value === null || typeof value !== 'object') { |
| 46 | throw new Error('body must be a JSON object') |
| 47 | } |
| 48 | if ( |
| 49 | !('threadId' in value) || |
| 50 | typeof value.threadId !== 'string' || |
| 51 | value.threadId === '' |
| 52 | ) { |
| 53 | throw new Error('body.threadId must be a non-empty string') |
| 54 | } |
| 55 | if ( |
| 56 | !('messages' in value) || |
| 57 | !Array.isArray(value.messages) || |
| 58 | value.messages.length === 0 |
| 59 | ) { |
| 60 | throw new Error('body.messages must be a non-empty array') |
| 61 | } |
| 62 | // The chat engine validates message shape; we only assert it is an array of |
| 63 | // objects here so the request fails fast with a clear 400 on garbage input. |
| 64 | for (const message of value.messages) { |
| 65 | if (message === null || typeof message !== 'object') { |
| 66 | throw new Error('each message must be an object') |
| 67 | } |
| 68 | } |
| 69 | // Optional free-form pass-through (app-validated). Must be an object if present. |
| 70 | let metadata: Record<string, unknown> | undefined |
| 71 | if ('metadata' in value && value.metadata !== undefined) { |
| 72 | if (!isRecord(value.metadata)) { |
| 73 | throw new Error('body.metadata must be an object') |
| 74 | } |
| 75 | metadata = value.metadata |
| 76 | } |
| 77 | return { threadId: value.threadId, messages: value.messages, metadata } |
| 78 | } |
| 79 | |
| 80 | /** A JSON object — narrows `unknown` to `Record<string, unknown>` cast-free. */ |
| 81 | function isRecord(value: unknown): value is Record<string, unknown> { |