rodiumai

Official Dart/Flutter SDK for the Rodium AI API — access GPT, Claude, Gemini, DeepSeek and more with RODI credits and Mobile Money top-ups.

pub package Dart SDK License: MIT Tests

OpenAI-compatible REST API — same endpoints and payloads as documented at rodiumai.io/docs.

Resource URL
pub.dev pub.dev/packages/rodiumai
Source code github.com/lecodeur228/rodiumai
API documentation rodiumai.io/docs
Dashboard & API keys rodiumai.io/dashboard
Model catalogue rodiumai.io/models

Table of contents


Installation

flutter pub add rodiumai

Requirements: Dart SDK >=3.0.0. Pure Dart package — works in Flutter, server, and CLI alike.


Flutter app integration

Typical flow in a real app:

main.dart          → load API key, create RodiumAIClient
       ↓
ChatScreen         → load models from API, dropdown, send message
       ↓
RodiumAIClient     → chat() or stream() → update UI with setState

1. Bootstrap — main.dart

import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:rodiumai/rodiumai.dart';

import 'chat_screen.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await dotenv.load(fileName: '.env');

  final client = RodiumAIClient(
    apiKey: dotenv.env['RODIUMAI_API_KEY']!,
    // locale: 'fr', // optional — SDK error hints only
  );

  runApp(RodiumAIApp(client: client));
}

class RodiumAIApp extends StatelessWidget {
  const RodiumAIApp({super.key, required this.client});
  final RodiumAIClient client;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'RodiumAI Chat',
      theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
      home: ChatScreen(client: client),
    );
  }
}

.env (never commit):

RODIUMAI_API_KEY=rd_sk_votre_cle

pubspec.yaml assets:

dependencies:
  rodiumai: ^0.1.0
  flutter_dotenv: ^5.2.0

flutter:
  assets:
    - .env

Alternative without dotenv — --dart-define at build time:

const apiKey = String.fromEnvironment('RODIUMAI_API_KEY');
final client = RodiumAIClient(apiKey: apiKey);
flutter run --dart-define=RODIUMAI_API_KEY=rd_sk_votre_cle

Chat screen (full example)

Complete screen: loads models from the API, model picker, message input, response area, error handling.

import 'package:flutter/material.dart';
import 'package:rodiumai/rodiumai.dart';

class ChatScreen extends StatefulWidget {
  const ChatScreen({super.key, required this.client});
  final RodiumAIClient client;

  @override
  State<ChatScreen> createState() => _ChatScreenState();
}

class _ChatScreenState extends State<ChatScreen> {
  final _controller = TextEditingController();
  final _scrollController = ScrollController();

  List<ModelInfo> _models = [];
  String? _selectedModelId;
  String _response = '';
  bool _loadingModels = true;
  bool _loadingChat = false;

  @override
  void initState() {
    super.initState();
    _loadModels();
  }

  Future<void> _loadModels() async {
    try {
      final catalogue = await widget.client.models();
      final preferred = catalogue.preferredChatModel;
      setState(() {
        _models = catalogue.chatModels;
        _selectedModelId = preferred?.id;
        _loadingModels = false;
      });
    } on RodiumAIException catch (e) {
      setState(() => _loadingModels = false);
      _showError('Impossible de charger les modèles (${e.statusCode})');
    }
  }

  Future<void> _sendMessage() async {
    final prompt = _controller.text.trim();
    if (prompt.isEmpty || _selectedModelId == null || _loadingChat) return;

    setState(() {
      _loadingChat = true;
      _response = '';
    });

    try {
      final locale = Localizations.localeOf(context).languageCode;
      final res = await widget.client
          .language(locale) // optional — force app language
          .model(_selectedModelId!)
          .temperature(0.7)
          .maxTokens(512)
          .chat(prompt);

      setState(() => _response = res.content);
      _scrollToBottom();
    } on InsufficientCreditsException {
      _showError('Crédits RODI insuffisants — rechargez sur rodiumai.io');
    } on UnauthorizedException {
      _showError('Clé API invalide');
    } on RodiumAIException catch (e) {
      setState(() => _response = 'Erreur (${e.statusCode}): ${e.message}');
    } finally {
      setState(() => _loadingChat = false);
    }
  }

  void _showError(String message) {
    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
  }

  void _scrollToBottom() {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (_scrollController.hasClients) {
        _scrollController.animateTo(
          _scrollController.position.maxScrollExtent,
          duration: const Duration(milliseconds: 300),
          curve: Curves.easeOut,
        );
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('RodiumAI'),
        actions: [
          if (_loadingModels)
            const Padding(
              padding: EdgeInsets.all(16),
              child: SizedBox(
                width: 20,
                height: 20,
                child: CircularProgressIndicator(strokeWidth: 2),
              ),
            ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            // ── Model picker (data from GET /v1/models) ──
            if (_models.isNotEmpty)
              DropdownButtonFormField<String>(
                value: _selectedModelId,
                decoration: const InputDecoration(
                  labelText: 'Modèle IA',
                  border: OutlineInputBorder(),
                ),
                items: _models
                    .map(
                      (m) => DropdownMenuItem(
                        value: m.id,
                        child: Text('${m.displayName} · ${m.providerPrefix}'),
                      ),
                    )
                    .toList(),
                onChanged: _loadingChat ? null : (v) => setState(() => _selectedModelId = v),
              ),
            const SizedBox(height: 12),

            // ── User input ──
            TextField(
              controller: _controller,
              enabled: !_loadingChat && _selectedModelId != null,
              decoration: const InputDecoration(
                labelText: 'Votre message',
                border: OutlineInputBorder(),
                hintText: 'Posez une question…',
              ),
              maxLines: 3,
              onSubmitted: (_) => _sendMessage(),
            ),
            const SizedBox(height: 12),

            FilledButton.icon(
              onPressed: _loadingChat ? null : _sendMessage,
              icon: _loadingChat
                  ? const SizedBox(
                      width: 18,
                      height: 18,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    )
                  : const Icon(Icons.send),
              label: Text(_loadingChat ? 'Génération…' : 'Envoyer'),
            ),
            const SizedBox(height: 16),

            // ── AI response ──
            Expanded(
              child: Container(
                padding: const EdgeInsets.all(12),
                decoration: BoxDecoration(
                  border: Border.all(color: Theme.of(context).dividerColor),
                  borderRadius: BorderRadius.circular(8),
                ),
                child: SingleChildScrollView(
                  controller: _scrollController,
                  child: Text(
                    _response.isEmpty ? 'La réponse apparaîtra ici…' : _response,
                    style: Theme.of(context).textTheme.bodyLarge,
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    _scrollController.dispose();
    super.dispose();
  }
}

Provider filter chips (optional UI)

Widget buildProviderChips(
  ModelCollection catalogue,
  ValueChanged<String> onModelSelected,
) {
  return Wrap(
    spacing: 8,
    children: catalogue.providerPrefixes.map((prefix) {
      final models = catalogue.byProvider(prefix).chatModels;
      return ActionChip(
        label: Text('$prefix (${models.length})'),
        onPressed: models.isEmpty ? null : () => onModelSelected(models.first.id),
      );
    }).toList(),
  );
}

Streaming in the UI

Typing effect — update the widget on each SSE delta:

class StreamingBubble extends StatefulWidget {
  const StreamingBubble({super.key, required this.client, required this.modelId});
  final RodiumAIClient client;
  final String modelId;

  @override
  State<StreamingBubble> createState() => _StreamingBubbleState();
}

class _StreamingBubbleState extends State<StreamingBubble> {
  final _text = StringBuffer();
  bool _streaming = false;

  Future<void> _startStream(String prompt) async {
    setState(() {
      _text.clear();
      _streaming = true;
    });

    try {
      await for (final delta in widget.client.model(widget.modelId).stream(prompt)) {
        setState(() => _text.write(delta.content));
      }
    } on RodiumAIException catch (e) {
      setState(() => _text.write('\n[Erreur ${e.statusCode}]'));
    } finally {
      setState(() => _streaming = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        FilledButton(
          onPressed: _streaming ? null : () => _startStream('Raconte une histoire courte.'),
          child: Text(_streaming ? 'En cours…' : 'Générer en streaming'),
        ),
        const SizedBox(height: 12),
        Expanded(
          child: SingleChildScrollView(
            child: Text(_text.toString().isEmpty ? '…' : _text.toString()),
          ),
        ),
      ],
    );
  }
}

Dynamic models from API

No static enum — the catalogue is always fetched live from GET /v1/models:

final catalogue = await client.models();

catalogue.preferredChatModel;     // recommended: openai/gpt-4o if available
catalogue.chatModels;             // text/chat-capable models
catalogue.byProvider('anthropic');  // filter by provider prefix
catalogue.findById('openai/gpt-4o');

Use preferredChatModel in your dropdown default — avoids picking a model that may be unavailable server-side.


Configuration & API key

  1. Create an account on rodiumai.io/dashboard
  2. Top up RODI credits (Mobile Money: MTN, Orange, Wave, Moov, Airtel…)
  3. Generate an API key (rd_sk_…)
  4. Store it in .env or --dart-definenever commit secrets
final client = RodiumAIClient(apiKey: apiKey);

Multilingual (optional)

Language is never required.

Need How
Localized SDK error hints RodiumAIClient(apiKey: key, locale: 'fr')
AI replies in app language .language(Localizations.localeOf(context).languageCode) before chat()

Supported: en (default), fr, es.


Error handling

HTTP Exception What to show in the app
401 UnauthorizedException Invalid API key
402 InsufficientCreditsException Link to dashboard to top up
429 RateLimitException Retry later
422 ValidationException Check model / messages
500+ RodiumAIException Generic error + retry
try {
  await client.model(modelId).chat(prompt);
} on InsufficientCreditsException {
  // Show SnackBar + link to recharge
} on RodiumAIException catch (e) {
  // Show e.message in UI
}

API reference

RodiumAIClient

Method Description
chat(messages) Non-streaming completion
stream(messages) SSE streaming → Stream<ChatStreamDelta>
models() Live catalogue → ModelCollection
model(id) Set model (fluent)
temperature(n) / topP(n) / maxTokens(n) Generation params
systemPrompt(s) System message prepended
language(code) Optional — AI response language

messages: String, List<ChatMessage>, or List<Map>.

ModelCollection

Member Description
preferredChatModel Recommended default model
chatModels Chat-capable models
byProvider(prefix) Filter e.g. "openai"
findById(id) Lookup by ID
providerPrefixes All providers from API

ChatMessage

ChatMessage.user('Hello');
ChatMessage.assistant('Previous reply');
ChatMessage.system('You are a helpful assistant.');

Endpoints

API SDK
POST /v1/chat/completions chat(), stream()
GET /v1/models models()

Base URL: https://api.rodiumai.io/v1 · Auth: Bearer {RODIUMAI_API_KEY}


Contributing

See CONTRIBUTING.md.

dart pub get && dart test && dart analyze --fatal-infos

License

MIT — see LICENSE.

Libraries

rodiumai
SDK officiel Dart/Flutter pour l'API RodiumAI.