CookbookCancel a stream

Cancel a stream

To stop a reply partway (a Stop button, a user who navigated away, a time limit), close the connection. The model stops generating shortly afterwards. There is no separate cancel endpoint.

Tokens generated before the connection closed are billed. If you disconnect before the final usage chunk arrives, Syntic bills an estimate based on the text sent so far and the size of your request.

const controller = new AbortController()
stopButton.onclick = () => controller.abort()               // user pressed Stop
const timer = setTimeout(() => controller.abort(), 60_000)  // or a 60 s limit
 
let text = ''
try {
  const res = await fetch('/api/ask', {                     // your backend, which streams from Syntic
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ q: 'Tell me a long story about a lighthouse keeper.' }),
    signal: controller.signal,
  })
  const reader = res.body.pipeThrough(new TextDecoderStream()).getReader()
  for (let r = await reader.read(); !r.done; r = await reader.read()) {
    text += r.value
    output.textContent = text
  }
} catch (err) {
  if (err.name !== 'AbortError') throw err
  output.textContent = text + ' [stopped]'
} finally {
  clearTimeout(timer)
}

When your backend relays the stream, also stop the upstream request when the browser goes away, or the model keeps writing (and billing) for nobody:

// Express route (server side)
app.post('/api/ask', async (req, res) => {
  const upstreamAbort = new AbortController()
  res.on('close', () => upstreamAbort.abort())              // browser disconnected or aborted
  try {
    const upstream = await fetch('https://app.syntic.ai/v1/chat/completions', {
      method: 'POST',
      headers: { Authorization: `Bearer ${process.env.SYNTIC_API_KEY}`, 'Content-Type': 'application/json' },
      body: JSON.stringify({ model: 'Amara Quick', messages: [{ role: 'user', content: String(req.body.q) }] }),
      signal: upstreamAbort.signal,
    })
    // ...parse events and res.write() the text pieces, as in "Stream in Node.js"
  } catch (err) {
    if (err.name !== 'AbortError') console.error(err)
  }
})

Keeping what was received

A cancelled reply is still useful text. If you store the conversation, save the partial assistant message with a marker (for example [stopped by user]) so the next turn has context. Because the reply never finished, there is no finish_reason for it.

Was this page helpful?