infro

The official Dart client for the INFRO API — one endpoint for text, image, video and audio models.

dart pub add infro
import 'package:infro/infro.dart';

final infro = Infro();  // reads INFRO_API_KEY

final image = await infro.images.generate(
  model: 'bfl/flux-2-pro',
  prompt: 'a lighthouse in fog, 35mm',
);

print(image['data'][0]['url']);
print("cost: \$${image['usage']['cost']}");

Why Dart is on the list

Because Flutter is. A mobile app that calls model APIs wants the multimodal surface and the per-request cost figure in the same place, and the alternative is hand-rolled JSON in a widget. One dependency — package:http, which a Flutter app already carries.

Configuration

final infro = Infro(
  apiKey: 'sk_infro_...',            // or INFRO_API_KEY in the environment
  baseUrl: 'https://api.infro.io/v1',
  timeout: Duration(minutes: 10),     // per attempt
  maxRetries: 2,                      // attempts *after* the first
);

The key falls back to INFRO_API_KEY because a key in source is a key in version control.

On Flutter for web there is no Platform.environment, so pass apiKey explicitly — and remember that a key shipped in a web bundle is a public key. Proxy through your own backend instead.

Text

final completion = await infro.chat.completions.create(
  model: 'anthropic/claude-sonnet-5',
  messages: [
    {'role': 'user', 'content': 'Hello'},
  ],
);

print(completion['choices'][0]['message']['content']);
print(completion['model']);  // the model that *served* — matters with fallbacks
print(completion['provider']);  // the provider kind of your connection that served
print(completion['route']);  // "primary" or "standby_a" — its position in the chain

Streaming is a Stream, which is what every Flutter widget already knows how to consume:

final stream = infro.chat.completions.createStream(
  model: 'anthropic/claude-sonnet-5',
  messages: [
    {'role': 'user', 'content': 'Write a haiku about fog.'},
  ],
);

await for (final chunk in stream) {
  stdout.write(chunk['choices'][0]['delta']['content'] ?? '');
}

A stream that ends without [DONE] throws rather than completing quietly: the gateway closes that way when a request fails after the first byte, so silence would hand you a truncated answer that looks complete.

Routing, fallbacks and cost control

final completion = await infro.chat.completions.create(
  model: 'openai/gpt-5.6-terra',
  messages: [{'role': 'user', 'content': 'Hello'}],
  fallbacks: ['deepseek/deepseek-v4-flash'],
  metadata: {'user': 'u_42', 'feature': 'summariser'},
  logging: false,  // this request's content is never stored
);

Video

Renders take minutes, so they are jobs. A webhook is the production path:

final job = await infro.videos.create(
  model: 'kuaishou/kling-o3',
  prompt: 'slow push-in on a lighthouse in fog',
  durationSeconds: 6,
  webhook: {'url': 'https://api.example.com/hooks/infro'},
);

In a script or a test, where there is nowhere for a webhook to land, poll:

final finished = await infro.jobs.waitFor(job['id'] as String);
print(finished['output']['url']);

waitFor throws on a failed render rather than returning it, because a caller who awaited "the finished video" will use whatever comes back as one.

Audio

final bytes = await infro.audio.speech(
  model: 'elevenlabs/eleven-v3',
  input: 'The lighthouse keeper watched the fog roll in.',
  voice: 'rachel',
);
await File('out.mp3').writeAsBytes(bytes);

Errors

try {
  await infro.chat.completions.create(model: model, messages: messages);
} on BudgetExceededException catch (error) {
  // a budget you set was reached: raise it or wait for its period to reset;
  // retrying will not help
} on RateLimitException catch (error) {
  // already retried for you; error.retryAfter is the server's own hint
} on InfroApiException catch (error) {
  print(error.requestId);  // the first thing support will ask for
} on InfroConnectionException {
  // never reached the gateway — DNS, TLS, a dropped socket
}

Every exception carries status, type, code, requestId and retryable. retryable is keyed on the HTTP status, not the type, because the status is what the docs promise and what a proxy preserves.

Retries

Only the four statuses the docs name as retryable — 408, 429, 502, 503 — and always with an Idempotency-Key, so a retry of a request the gateway already accepted returns the first response instead of doing the work twice. Retry-After wins over the computed backoff; jitter stops a fleet that failed together from retrying together.

Streams are never retried: past the first byte the caller has already seen part of an answer.

Anything this client does not model

The gateway grows faster than a client library, so there is an escape hatch that keeps the authentication and the retries:

final usage = await infro.transport.request('GET', '/usage?from=2026-08-01');

Documentation

infro.io/docs — the API reference, the error table, and the routing, fallback and privacy semantics this client is a thin shell over.

Libraries

infro
infro — the official Dart client for the INFRO API.