A unified Flutter/Dart wrapper for integrating various AI APIs (OpenAI, Anthropic, Google AI) with streaming, context management, and multimodal support.
- 🔄 Unified API - Single interface for multiple AI providers
- 🏠 Local Models - Run Llama, Qwen, Gemma... locally via Ollama
- 🌊 Streaming Support - Real-time response streaming
- 💬 Context Management - Automatic conversation history and memory
- 🖼️ Multimodal Support - Text, images, audio, and documents
- 🛠️ Function Calling - Tool/function support for all providers
- 🤖 Tool Runner - Automatic agentic tool-calling loop
- 🔒 Type Safety - Full Dart type safety with null safety
- ⚡ Error Handling - Comprehensive error types and retry logic
- 📊 Token Counting - Exact counts via provider endpoints (Anthropic, Google AI) or local estimation
- 💾 Prompt Caching - Up to 90% cheaper repeated contexts (
PromptCaching)
| Provider | Text | Vision | Audio | Tools | Streaming |
|---|---|---|---|---|---|
| OpenAI | ✅ | ✅ | ✅ | ✅ | ✅ |
| Anthropic | ✅ | ✅ | ❌ | ✅ | ✅ |
| Google AI | ✅ | ✅ | ✅ | ✅ | ✅ |
| Ollama (local) | ✅ | ✅ | ❌ | ✅ | ✅ |
Add to your pubspec.yaml:
dependencies:
flutter_ai_sdk: ^1.0.0Or run:
flutter pub add flutter_ai_sdkimport 'package:flutter_ai_sdk/flutter_ai_sdk.dart';
// Initialize the SDK
final ai = FlutterAI(
provider: AIProvider.openai,
config: AIConfig(
apiKey: 'your-api-key',
model: 'gpt-5.5',
),
);
// Simple chat
final response = await ai.chat('Hello, how are you?');
print(response.text);
// Don't forget to dispose when done
ai.dispose();final ai = FlutterAI(
provider: AIProvider.anthropic,
config: AIConfig(
apiKey: 'your-api-key',
model: 'claude-opus-4-8',
),
);
// Stream responses
await for (final chunk in ai.streamChat('Tell me a story')) {
if (chunk.isDelta) {
print(chunk.delta); // Print each chunk as it arrives
}
}final ai = FlutterAI(
provider: AIProvider.openai,
config: AIConfig(
apiKey: 'your-api-key',
systemPrompt: 'You are a helpful coding assistant.',
),
);
// Context is automatically maintained
await ai.chat('What is Dart?');
await ai.chat('Can you show me an example?');
await ai.chat('How does it compare to JavaScript?');
// Access conversation history
print(ai.history.length);final ai = FlutterAI(
provider: AIProvider.openai,
config: AIConfig(
apiKey: 'your-api-key',
model: 'gpt-5.5',
),
);
// Analyze an image from URL
final response = await ai.chatWithContent([
TextContent('What is in this image?'),
ImageContent.fromUrl('https://example.com/image.png'),
]);
// Or from local bytes
final imageBytes = await File('image.png').readAsBytes();
final response = await ai.chatWithContent([
TextContent('Describe this image'),
ImageContent.fromBytes(imageBytes, mimeType: 'image/png'),
]);| Provider | Base64 (PDF...) | URL |
|---|---|---|
| Anthropic | ✅ | ✅ |
| Google AI | ✅ | ✅ |
| OpenAI | ✅ | |
| Ollama | ❌ | ❌ |
final response = await ai.chatWithContent([
TextContent('Summarize this report'),
DocumentContent.fromBase64(base64Pdf, mimeType: 'application/pdf', name: 'report.pdf'),
]);// Define a tool
final weatherTool = Tool(
name: 'get_weather',
description: 'Get the current weather for a location',
parameters: ToolParameters(
properties: {
'location': ToolProperty.string(
description: 'The city and country, e.g., "Paris, France"',
),
'unit': ToolProperty.enumeration(
description: 'Temperature unit',
values: ['celsius', 'fahrenheit'],
),
},
required: ['location'],
),
);
// Use the tool
final response = await ai.chatWithTools(
'What is the weather in Paris?',
tools: [weatherTool],
);
// Handle tool calls
if (response.hasToolCalls) {
for (final call in response.toolCalls!) {
// Execute the tool
final result = await executeWeatherCall(call.arguments);
// Submit result back to the AI
final finalResponse = await ai.submitToolResult(
toolCallId: call.id,
name: call.name,
result: result,
);
print(finalResponse.text);
}
}Let the SDK run the full agentic loop for you: it executes every tool call the model requests and feeds the results back until the model produces a final answer.
final runner = ToolRunner.create(
provider: AIProvider.anthropic,
config: AIConfig(apiKey: 'sk-ant-...'),
tools: [
ExecutableTool(
definition: weatherTool,
executor: (args) async => fetchWeather(args['location'] as String),
),
],
);
final result = await runner.run('What is the weather in Paris?');
print(result.text); // Final answer
print(result.iterations); // Number of tool roundstry {
final response = await ai.chat('Hello');
} on AIAuthenticationError catch (e) {
print('Invalid API key: ${e.message}');
} on AIRateLimitError catch (e) {
print('Rate limited. Retry after: ${e.retryAfter}');
await Future.delayed(e.retryAfter ?? Duration(seconds: 60));
// Retry the request
} on AIContextLengthError catch (e) {
print('Context too long: ${e.message}');
ai.clearContext(); // Clear and retry
} on AIError catch (e) {
print('AI error: ${e.message}');
}final config = AIConfig(
// Required
apiKey: 'your-api-key',
// Model selection
model: 'gpt-5.5', // Provider-specific model name
// Generation parameters
maxTokens: 4096,
temperature: 0.7, // 0.0 - 2.0, higher = more random
topP: 0.9, // Alternative to temperature
frequencyPenalty: 0.0, // -2.0 to 2.0
presencePenalty: 0.0, // -2.0 to 2.0
stopSequences: ['END'], // Stop generation at these sequences
// System behavior
systemPrompt: 'You are a helpful assistant.',
// Response format
responseFormat: ResponseFormat.json(), // JSON mode
// Or guaranteed structured output (OpenAI, Anthropic, Google AI, Ollama):
// responseFormat: ResponseFormat.json(schema: {
// 'type': 'object',
// 'properties': {'name': {'type': 'string'}},
// 'required': ['name'],
// }),
// Tools/Functions
tools: [myTool],
toolChoice: ToolChoice.auto(),
// Network settings
baseUrl: 'https://custom-endpoint.com', // Custom API endpoint
timeout: Duration(seconds: 30),
headers: {'X-Custom-Header': 'value'},
);Pass a JSON schema to get responses that are guaranteed to match it —
each provider uses its native structured output mechanism (OpenAI
json_schema, Anthropic output_config, Gemini responseJsonSchema,
Ollama schema format):
final ai = FlutterAI(
provider: AIProvider.openai,
config: AIConfig(
apiKey: 'sk-...',
responseFormat: ResponseFormat.json(
schema: {
'type': 'object',
'properties': {
'name': {'type': 'string'},
'age': {'type': 'integer'},
},
'required': ['name', 'age'],
},
strict: true, // strict validation (OpenAI)
),
),
);
final response = await ai.chat('Extract: John Smith, 42 years old');
final data = jsonDecode(response.text); // guaranteed validCache the repeated prefix of your prompts (long system prompt, documents, conversation history) to cut input costs by up to ~90%:
final ai = FlutterAI(
provider: AIProvider.anthropic,
config: AIConfig(
apiKey: 'sk-ant-...',
systemPrompt: veryLongSystemPrompt,
promptCaching: PromptCaching(), // or PromptCaching(ttl: PromptCacheTtl.oneHour)
),
);
final response = await ai.chat('First question');
print(response.usage?.cacheWriteTokens); // prefix written to the cache
final followUp = await ai.chat('Second question');
print(followUp.usage?.cachedTokens); // prefix read from the cache (~10% of the price)| Provider | Behavior |
|---|---|
| Anthropic | Explicit — enabled by promptCaching (5 min or 1 h TTL) |
| OpenAI | Automatic for prompts ≥ ~1024 tokens; hits reported in usage.cachedTokens |
| Google AI | Implicit caching automatic; hits reported in usage.cachedTokens |
| Ollama | Local KV-cache, always on |
Count tokens before sending a request — Anthropic and Google AI use their exact server-side counting endpoints; other providers fall back to a local estimation:
final tokens = await ai.countTokens(message: 'My long prompt...');
if (tokens > 100000) {
// Trim the context before sending
}The SDK includes built-in context management to handle conversation history:
final ai = FlutterAI(
provider: AIProvider.openai,
config: AIConfig(apiKey: 'your-key'),
);
// Messages are automatically tracked
await ai.chat('Hello');
await ai.chat('Tell me more');
// Access the context manager
print(ai.context.estimatedTokens);
print(ai.context.availableTokens);
// Clear context
ai.clearContext();
// Reset with new system prompt
ai.reset(systemPrompt: 'New personality');
// Get conversation for serialization
final json = ai.conversation.toJson();final contextManager = ContextManager(
maxTokens: 8000,
reservedTokens: 1000, // Reserve for response
systemPrompt: 'You are helpful.',
windowStrategy: WindowStrategy.slidingWindow,
);
final ai = FlutterAI(
provider: AIProvider.openai,
config: AIConfig(apiKey: 'your-key'),
contextManager: contextManager,
);
// Listen to context updates
contextManager.updates.listen((update) {
print('Context updated: ${update.type}');
print('Messages: ${update.messageCount}');
print('Tokens: ${update.estimatedTokens}');
});final ai = FlutterAI(
provider: AIProvider.openai,
config: AIConfig(
apiKey: 'sk-...',
model: 'gpt-5.5', // or gpt-5.4, gpt-5.4-mini, etc.
),
);Supported models: gpt-5.5, gpt-5.4, gpt-5.4-mini, gpt-5.4-nano, gpt-5.1
final ai = FlutterAI(
provider: AIProvider.anthropic,
config: AIConfig(
apiKey: 'sk-ant-...',
model: 'claude-opus-4-8',
),
);Supported models: claude-opus-4-8, claude-sonnet-5, claude-sonnet-4-6, claude-haiku-4-5
final ai = FlutterAI(
provider: AIProvider.googleAI,
config: AIConfig(
apiKey: 'your-google-ai-key',
model: 'gemini-3.5-flash',
),
);Supported models: gemini-3.5-flash, gemini-3.1-pro-preview, gemini-3.1-flash-lite
Run open models locally — no API key, no cloud.
final ai = FlutterAI(
provider: AIProvider.ollama,
config: AIConfig(
apiKey: '', // not required
model: 'llama3.1',
// baseUrl: 'http://192.168.1.10:11434/api', // remote Ollama server
),
);Popular models: llama3.1, deepseek-r1, qwen3, gemma3, qwen3-coder
The SDK is organized in small, focused modules:
lib/src/
├── config/ # AIConfig, response formats, per-provider defaults
├── models/ # Messages, content types (sealed), tools, responses
├── providers/ # One folder per provider: thin provider + wire mapper
│ ├── anthropic/ openai/ google_ai/ ollama/
│ └── provider_registry.dart # factory: AIProvider -> BaseProvider
├── runner/ # ToolRunner: automatic tool-calling loop
├── context/ # Conversation history and memory
├── errors/ # Typed error hierarchy
└── utils/ # HTTP client (retry, SSE/NDJSON), token counting
Key design points:
- Strategy + template method -
BaseProviderowns the streaming loop; each provider only implements its transport and wire format - Mappers - request building / response parsing are stateless classes, isolated from HTTP concerns and independently testable
- Factory registry -
ProviderRegistry.registerlets you plug custom provider implementations without forking the SDK
class ChatWidget extends StatefulWidget {
@override
_ChatWidgetState createState() => _ChatWidgetState();
}
class _ChatWidgetState extends State<ChatWidget> {
late FlutterAI _ai;
final _messages = <Message>[];
String _streamingContent = '';
bool _isLoading = false;
@override
void initState() {
super.initState();
_ai = FlutterAI(
provider: AIProvider.openai,
config: AIConfig(apiKey: 'your-key'),
);
}
Future<void> _sendMessage(String text) async {
setState(() {
_messages.add(Message.user(text));
_isLoading = true;
_streamingContent = '';
});
await for (final chunk in _ai.streamChat(text)) {
if (chunk.isDelta) {
setState(() {
_streamingContent += chunk.delta ?? '';
});
}
if (chunk.isDone) {
setState(() {
_messages.add(Message.assistant(_streamingContent));
_streamingContent = '';
_isLoading = false;
});
}
}
}
@override
void dispose() {
_ai.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: ListView.builder(
itemCount: _messages.length,
itemBuilder: (context, index) {
final message = _messages[index];
return ListTile(
title: Text(message.text),
subtitle: Text(message.role.name),
);
},
),
),
if (_streamingContent.isNotEmpty)
Padding(
padding: EdgeInsets.all(8),
child: Text(_streamingContent),
),
// Add your message input UI here
],
);
}
}| Method | Description |
|---|---|
chat(String message) |
Send a text message |
chatWithContent(List<Content> content) |
Send multimodal content |
chatWithTools(String message, {required List<Tool> tools}) |
Chat with tools |
submitToolResult({...}) |
Submit tool result |
streamChat(String message) |
Stream a response |
streamChatWithContent(List<Content> content) |
Stream multimodal |
clearContext() |
Clear conversation |
reset({String? systemPrompt}) |
Reset with new prompt |
| Type | Description |
|---|---|
TextContent(String text) |
Plain text |
ImageContent.fromUrl(String url) |
Image from URL |
ImageContent.fromBytes(Uint8List bytes) |
Image from bytes |
AudioContent.fromUrl(String url) |
Audio from URL |
DocumentContent.fromUrl(String url) |
Document from URL |
| Error | Description |
|---|---|
AIAuthenticationError |
Invalid API key |
AIRateLimitError |
Rate limit exceeded |
AIInvalidRequestError |
Bad request parameters |
AIContextLengthError |
Context too long |
AIContentFilterError |
Content blocked |
AINetworkError |
Network issues |
AIServerError |
Server errors |
- Always dispose - Call
ai.dispose()when done to release resources - Handle errors - Use try-catch for all API calls
- Monitor tokens - Check
context.estimatedTokensbefore sending large requests - Stream for long responses - Use
streamChatfor better UX - Secure API keys - Never hardcode keys, use environment variables or secure storage
- 📖 Documentation
- 🐛 Issues
- 💬 Discussions
This project is licensed under the MIT License.