| 276 | } |
| 277 | |
| 278 | export async function POST(req: Request) { |
| 279 | if (!isAllowedOrigin(req)) { |
| 280 | return new Response('Forbidden', { status: 403 }) |
| 281 | } |
| 282 | |
| 283 | const now = Date.now() |
| 284 | sweepRateLimit(now) |
| 285 | const retryAfter = rateLimit(getClientIp(req), now) |
| 286 | if (retryAfter !== null) { |
| 287 | return new Response('Too many requests', { |
| 288 | status: 429, |
| 289 | headers: { 'Retry-After': String(retryAfter) }, |
| 290 | }) |
| 291 | } |
| 292 | |
| 293 | let body: { messages: UIMessage[]; locale?: string } |
| 294 | try { |
| 295 | body = await req.json() |
| 296 | } catch { |
| 297 | return new Response('Invalid JSON', { status: 400 }) |
| 298 | } |
| 299 | const { messages } = body |
| 300 | const locale = KNOWN_LOCALES.includes(body.locale ?? '') |
| 301 | ? (body.locale as string) |
| 302 | : DEFAULT_LOCALE |
| 303 | |
| 304 | if (!Array.isArray(messages) || messages.length === 0 || messages.length > MAX_MESSAGES) { |
| 305 | return new Response('Invalid request', { status: 400 }) |
| 306 | } |
| 307 | if (!messages.every(isValidMessage)) { |
| 308 | return new Response('Invalid request', { status: 400 }) |
| 309 | } |
| 310 | if (userInputChars(messages) > MAX_USER_INPUT_CHARS) { |
| 311 | return new Response('Request too large', { status: 413 }) |
| 312 | } |
| 313 | |
| 314 | const modelMessages = sanitizeMessages(messages) |
| 315 | if (modelMessages.length === 0) { |
| 316 | return new Response('Invalid request', { status: 400 }) |
| 317 | } |
| 318 | // Bound what actually reaches the model. Measured AFTER sanitization, so the |
| 319 | // prior searchDocs tool outputs that accumulate in client history (and are |
| 320 | // stripped here) don't count — only user/assistant text the model will see. |
| 321 | if (JSON.stringify(modelMessages).length > MAX_TOTAL_CHARS) { |
| 322 | return new Response('Request too large', { status: 413 }) |
| 323 | } |
| 324 | |
| 325 | const result = streamText({ |
| 326 | model: openai(CHAT_MODEL), |
| 327 | system: SYSTEM_PROMPT, |
| 328 | messages: convertToModelMessages(modelMessages), |
| 329 | stopWhen: stepCountIs(MAX_STEPS), |
| 330 | maxOutputTokens: MAX_OUTPUT_TOKENS, |
| 331 | tools: { |
| 332 | searchDocs: tool({ |
| 333 | description: |
| 334 | 'Search the Sim documentation for relevant content. Use this before answering any question about Sim.', |
| 335 | inputSchema: z.object({ |