rodiumai 0.1.1 copy "rodiumai: ^0.1.1" to clipboard
rodiumai: ^0.1.1 copied to clipboard

Official Dart/Flutter SDK for the RodiumAI API. Access GPT, Claude, Gemini, DeepSeek and more — pay with Mobile Money via RODI credits.

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.

0
likes
145
points
189
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Official Dart/Flutter SDK for the RodiumAI API. Access GPT, Claude, Gemini, DeepSeek and more — pay with Mobile Money via RODI credits.

License

MIT (license)

Dependencies

dio, meta

More

Packages that depend on rodiumai