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.
OpenAI-compatible REST API — same endpoints and payloads as documented at rodiumai.io/docs.
Links
| 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 app integration
- Chat screen (full example)
- Streaming in the UI
- Dynamic models from API
- Configuration & API key
- Multilingual (optional)
- Error handling
- API reference
- Contributing
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
- Create an account on rodiumai.io/dashboard
- Top up RODI credits (Mobile Money: MTN, Orange, Wave, Moov, Airtel…)
- Generate an API key (
rd_sk_…) - Store it in
.envor--dart-define— never 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.