Chat API

The chat API is the same endpoint the hosted UI uses to capture prompts, stream model responses, and emit DSL deltas. It is a Server‑Sent Events (SSE) interface: the request body is JSON, the response body is a stream of data: records that you parse incrementally.

Authentication

  • • Requests require the same Supabase session cookie used by the browser app. Log in via the UI or run weldr login before issuing API calls.
  • • Automated tests can impersonate a seeded account in development by passing X-Test-User: . This header is ignored in production.
  • POST /api/chat

    Send a user message and receive a streaming response that mixes text deltas, DSL updates, and capability traces.

    Request Schema

    interface ChatRequest {
      id: string; // chatId (uuid). Supply your own via crypto.randomUUID().
      message: {
        id: string; // messageId (uuid)
        role: 'user';
        parts: Array<
          | { type: 'text'; text: string }                    // plain text (primary path)
          | { type: 'file'; mediaType: 'image/jpeg' | 'image/png'; name: string; url: string }
        >;
      };
      selectedChatModel: 'chat-model' | 'chat-model-reasoning';
      selectedVisibilityType: 'public' | 'private';
    }

    All fields are required by app/(chat)/api/chat/schema.ts. Omit optional behaviors like temperature or DSL toggles—the server chooses sane defaults (DSL extraction is always on unless ENABLE_DSL_EXTRACTION=false).

    Example Request

    curl -N -X POST http://localhost:3000/api/chat \
      -H "Content-Type: application/json" \
      -H "Cookie: sb-access-token=...; sb-refresh-token=..." \
      -d '{
            "id": "ad53cec3-7819-46e2-92f7-0b03036ad6ce",
            "message": {
              "id": "b52be0c7-e0c9-45a8-aba3-3e6d530467c1",
              "role": "user",
              "parts": [
                { "type": "text", "text": "Build a helpdesk with escalations and SLAs" }
              ]
            },
            "selectedChatModel": "chat-model",
            "selectedVisibilityType": "private"
          }'

    SSE Event Types

    | Event type | Payload | Description | | ------------ | ------- | ----------- | | text-start | { id } | Assistant placeholder message created in the database. | | text-delta | { id, delta } | Text tokens streamed from the orchestrator. Append delta strings in order to reconstruct the full response. | | message-metadata | { messageMetadata: { dsl_update \| phase_change \| dsl_sync_error } } | DSL deltas arrive inside dsl_update.delta (JSON Patch ops) with reasoning. Phase changes surface as phase_change.phase. Failures emit dsl_sync_error. | | data-uiIntentTrace | { trace } | Debug trace describing UI intents extracted from the conversation. | | data-capability-trace | { trace } | Similar telemetry for capability planners (notifications, automations, etc.). | | data-tool_call | { tool } | Low-level tool usage emitted by the orchestrator. | | text-end | { id } | Signals that textual streaming finished. | | finish | {} | Final envelope; close your SSE reader when this type arrives. | | error | { message } | Non-recoverable failure. The stream closes immediately afterwards. |

    Each line follows the SSE format data: {...}\n\n. When you receive a message-metadata.dsl_update, persist the delta or inspect messageMetadata.dsl_update.reasoning to understand why the DSL changed.

    Streaming Example (TypeScript)

    async function streamChat(body: ChatRequest) {
      const response = await fetch('/api/chat', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      });
    
      if (!response.body) throw new Error('No stream returned');
    
      const reader = response.body.getReader();
      const decoder = new TextDecoder();
    
      while (true) {
        const { value, done } = await reader.read();
        if (done) break;
    
        const chunk = decoder.decode(value, { stream: true });
        for (const line of chunk.split('\n')) {
          if (!line.startsWith('data: ')) continue;
          const event = JSON.parse(line.slice(6));
    
          switch (event.type) {
            case 'text-delta':
              appendText(event.delta);
              break;
            case 'message-metadata':
              if (event.messageMetadata.dsl_update) {
                hydrateDsl(event.messageMetadata.dsl_update.delta);
              }
              break;
            case 'finish':
              console.log('done');
              break;
          }
        }
      }
    }

    Error Responses

    The handler throws typed ChatSDKErrors which turn into JSON payloads:

    {
      "error": {
        "code": "rate_limit:chat",
        "message": "You have reached your daily limit of 50 messages. Please try again tomorrow or upgrade your account."
      }
    }

    Rate Limits

  • • Burst protection: max 10 requests per minute per user (enforceBurstRateLimit).
  • • Daily allowance: configurable via MAX_MESSAGES_* env vars (defaults: guests = 10, builders = 50, pro = 500). Limit enforcement lives in lib/rate-limit.ts.
  • • If the orchestrator cannot persist DSL changes, the stream keeps flowing but you will receive message-metadata.dsl_sync_error.
  • GET /api/chat/:id/stream

    Resume an interrupted stream. The resumable-stream layer stores the latest streamId for each chat; requesting /api/chat//stream will:

  • Validate the requester is the chat owner (private chats are locked to their creator).
  • Replay buffered SSE events when the previous stream is still active.
  • Fall back to emitting the last assistant message when the stream already ended (< 15 seconds ago).
  • Example:

    curl -N http://localhost:3000/api/chat/ad53cec3-7819-46e2-92f7-0b03036ad6ce/stream \
      -H "Cookie: sb-access-token=...; sb-refresh-token=..."

    Development & Testing Tips

  • • Supply deterministic UUIDs in tests so you can assert against stored rows in app_model_* tables.
  • • When running integration tests, set ENABLE_TEST_MODE=true and send X-Test-User: builder@test.dev to bypass standard rate limits.
  • • Toggle DSL extraction off by exporting ENABLE_DSL_EXTRACTION=false if you need to compare streaming behavior without schema mutations (rare).