Progress and long jobs
Every generate method takes an optional onProgress callback. You pass a
function, and the client calls it with a GenerationProgress each time the job
moves to a new stage. Use it to keep a UI or a CLI alive while a slow job runs.
The callback
The parameter is the same on all six capabilities:
void Function(GenerationProgress)? onProgress
So generateImage, generateVideo, generateMusic, and the rest all accept it.
A GenerationProgress carries three fields:
GenerationProgress({
required GenerationStage stage,
double? fraction,
String? message,
});
stage is the field you act on. fraction and message are optional and, on
most providers today, come through as null. Read Fraction and message
below before you build UI around them.
The stages
GenerationStage has four values, in lifecycle order:
queued: the provider accepted the job but has not started it.running: the provider is generating the result.downloading: the result is ready and the bytes are being fetched.done: the job finished andbytesare available.
Not every job passes through all four. Which ones you see depends on whether the provider answers in one request or makes you poll.
Immediate versus polling providers
Providers that return the result in a single request emit two updates:
running, then done.
Polling providers (Flux, Veo, Suno) submit a job and then poll a status URL until the result is ready. They emit more:
queuedbefore the first poll.runningbefore each poll after the first, so this repeats once per poll for as long as the job runs.downloadingonce, before the result bytes are fetched.doneonce the bytes are in hand.
A job that finishes on its very first poll skips running and goes
queued then downloading then done. So do not treat running as
guaranteed. Treat done as the terminal signal.
If a polling provider never finishes within its poll timeout, the generate call
throws AiTimeoutException instead of returning. See
Retries and timeouts for the timeout knobs.
Wiring it to a CLI status line
This example runs a real Flux image job and rewrites one terminal line as the
stage changes. It makes a real request and spends credits, so it needs
BFL_API_KEY in the environment.
import 'dart:io';
import 'package:ai_abstracted/ai_abstracted.dart';
Future<void> main() async {
final credentials = credentialsFromEnv(ProviderId.flux, Platform.environment);
if (credentials == null) {
stderr.writeln('Set BFL_API_KEY first.');
exit(1);
}
final flux = FluxImageClient(credentials: credentials);
final result = await flux.generateImage(
const ImageRequest(prompt: 'a red bicycle against a white wall'),
onProgress: (progress) => stdout.write('\r${_label(progress.stage)} '),
);
stdout.writeln();
await File('bicycle.png').writeAsBytes(result.bytes);
}
String _label(GenerationStage stage) => switch (stage) {
GenerationStage.queued => 'queued...',
GenerationStage.running => 'generating...',
GenerationStage.downloading => 'downloading...',
GenerationStage.done => 'done',
};
The \r returns the cursor to the start of the line, so each update overwrites
the last one instead of stacking. ai_abstracted itself uses no dart:io; the
file writing, stdout, and Platform.environment above are the caller's
choice, kept out of the package.
Fraction and message
fraction (a 0..1 estimate) and message (a status string) exist on
GenerationProgress, but the current provider clients do not populate them, so
they read as null. Do not gate a progress bar on fraction yet: check for
null and fall back to the stage.
onProgress: (progress) {
final percent = progress.fraction;
if (percent != null) {
stdout.write('\r${(percent * 100).round()}% ');
} else {
stdout.write('\r${progress.stage.name} ');
}
}
GenerationProgress has value equality, so you can skip a redundant redraw by
holding the last value and comparing before you write.