A stream
The same call, read while the model is still writing: the text a few characters at a time, then the whole result.
pnpm add @niscorp/signal zod openaiThe client
createSignal names a provider and gives you a client. The key is read from GROQ_API_KEY when a call is made, and the model is the provider’s default unless you name one with .model().
import { createSignal } from '@niscorp/signal';
const signal = createSignal('groq');A way to stop
A stream takes an AbortSignal. Aborting it ends the request to the provider, and the loop that reads the stream then throws.
const stop = new AbortController();
const timer = setTimeout(() => stop.abort(), 30_000);The events
stream is the same run as complete, as events. text carries what the model wrote since the last one. done carries what complete would have returned: the response, the history and the usage.
With a schema, the text is the JSON as it is written, and the typed object arrives with done. An answer that never passes the schema arrives as an error event. A provider’s failure, a missing key and an abort throw, so the loop stands in a try.
try {
for await (const event of signal.stream('Describe The Tempest in two sentences.', { signal: stop.signal })) {
if (event.type === 'text') process.stdout.write(event.text);
else if (event.type === 'done') console.log('\n' + event.meta.usage.totalTokens, 'tokens');
else if (event.type === 'error') console.log(event.error.message);
}
} catch (error) {
console.log('stopped:', error instanceof Error ? error.message : error);
} finally {
clearTimeout(timer);
}Run it.
This file asks a model, so it is not run on this site. It prints the two sentences as they arrive, then a line with the number of tokens.
GROQ_API_KEY=your-key npx tsx stream.mts