Integrations
Flutter & Dart
Use DEVUP AI from Dart and Flutter with the openai_dart or dart_openai package, and get chat, streaming, tool calling, structured output and embeddings on DEVUP AI models.
Keep your API key on your server
Never put your DEVUP AI key inside a Flutter app: anyone can extract it from the app package. Run the code on this page on your own server, and let your Flutter app call that server.
Requirements
- Dart 3.12 or newer for openai_dart 9.
- a DEVUP AI API key from https://devupai.com/dashboard/api-keys.
Install openai_dart
dart pub add openai_dartIn a Flutter project, run the same command with flutter instead of dart.
Set your key where your server runs:
PowerShell:
$env:DEVUP_API_KEY = "YOUR_DEVUP_API_KEY"Bash / Zsh:
export DEVUP_API_KEY="YOUR_DEVUP_API_KEY"Create a client
Import the package:
import 'dart:convert';
import 'dart:io';
import 'package:openai_dart/openai_dart.dart';/// Creates an openai_dart client that talks to DEVUP AI.
/// The key comes from the environment and is never written into source code.
OpenAIClient createClient() {
final apiKey = Platform.environment['DEVUP_API_KEY'];
if (apiKey == null || apiKey.isEmpty) {
throw StateError('DEVUP_API_KEY is not set');
}
return OpenAIClient(
config: OpenAIConfig(
authProvider: ApiKeyProvider(apiKey),
baseUrl: 'https://api.devupai.com/v1',
),
);
}
/// Sends one message and returns the model's reply.
Future<String> chat(OpenAIClient client) async {
final response = await client.chat.completions.create(
ChatCompletionCreateRequest(
model: 'deepseek-ai/DeepSeek-V4-Pro',
messages: [ChatMessage.user('Reply with exactly: Salam DEVUP')],
),
);
return response.text ?? '';
}chat returns Salam DEVUP.
Configure with environment variables
OpenAIClient.fromEnvironment() reads OPENAI_API_KEY and OPENAI_BASE_URL. Point them at DEVUP AI:
PowerShell:
$env:OPENAI_BASE_URL = "https://api.devupai.com/v1"
$env:OPENAI_API_KEY = "YOUR_DEVUP_API_KEY"Bash / Zsh:
export OPENAI_BASE_URL="https://api.devupai.com/v1"
export OPENAI_API_KEY="YOUR_DEVUP_API_KEY"/// Reads OPENAI_API_KEY and OPENAI_BASE_URL from the environment.
Future<String> chatFromEnvironment() async {
final client = OpenAIClient.fromEnvironment();
try {
final response = await client.chat.completions.create(
ChatCompletionCreateRequest(
model: 'deepseek-ai/DeepSeek-V4-Pro',
messages: [ChatMessage.user('Reply with exactly: Salam DEVUP')],
),
);
return response.text ?? '';
} finally {
client.close();
}
}Stream responses
/// Prints tokens as they arrive and returns the full text.
/// With includeUsage, token usage arrives once, in the final chunk.
Future<String> streamChat(OpenAIClient client) async {
final stream = client.chat.completions.createStream(
ChatCompletionCreateRequest(
model: 'deepseek-ai/DeepSeek-V4-Pro',
messages: [ChatMessage.user('Count from 1 to 5, separated by spaces.')],
streamOptions: const StreamOptions(includeUsage: true),
),
);
final accumulator = ChatStreamAccumulator();
await for (final event in stream) {
accumulator.add(event);
stdout.write(event.textDelta ?? '');
}
stdout.writeln();
final usage = accumulator.usage;
if (usage != null) {
final promptTokens = usage.promptTokens;
final completionTokens = usage.completionTokens;
print('tokens: $promptTokens in, $completionTokens out');
}
return accumulator.content;
}With includeUsage set, token usage arrives once, in the final chunk, and ChatStreamAccumulator returns it.
Tool calling
The model asks for get_order_status, your code runs it and sends the result back, and the model answers with the tracking number from your system.
/// Stands in for a lookup in your own system.
String getOrderStatus(String orderId) => jsonEncode({
'order_id': orderId,
'status': 'shipped',
'tracking_number': 'DZ-4471',
});
/// Lets the model call get_order_status, runs it locally, sends the result
/// back, and returns the model's final answer.
Future<String> toolRoundTrip(OpenAIClient client) async {
final tool = Tool.function(
name: 'get_order_status',
description: 'Look up the shipping status and tracking number of an order',
parameters: const {
'type': 'object',
'properties': {
'order_id': {'type': 'string'},
},
'required': ['order_id'],
},
);
final messages = <ChatMessage>[
ChatMessage.user('Where is my order 1042? Include its tracking number.'),
];
final first = await client.chat.completions.create(
ChatCompletionCreateRequest(
model: 'deepseek-ai/DeepSeek-V4-Pro',
messages: messages,
tools: [tool],
),
);
if (!first.hasToolCalls) {
throw StateError('The model answered without calling the tool');
}
// Every tool call must get a tool message back, including unknown ones.
messages.add(ChatMessage.assistant(toolCalls: first.allToolCalls));
for (final call in first.allToolCalls) {
var result = '{"error":"unknown tool"}';
if (call.function.name == 'get_order_status') {
final args = jsonDecode(call.function.arguments) as Map<String, dynamic>;
final orderId = args['order_id'] as String;
print('get_order_status("$orderId")');
result = getOrderStatus(orderId);
}
messages.add(ChatMessage.tool(toolCallId: call.id, content: result));
}
final second = await client.chat.completions.create(
ChatCompletionCreateRequest(
model: 'deepseek-ai/DeepSeek-V4-Pro',
messages: messages,
tools: [tool],
),
);
return second.text ?? '';
}Answer every tool call with a tool message, as the loop does, even for a tool name you do not recognise.
Structured output
Ask for JSON that matches a strict schema, then decode it.
/// Asks for JSON that matches a strict schema and decodes it.
Future<Map<String, dynamic>> structuredOutput(OpenAIClient client) async {
final response = await client.chat.completions.create(
ChatCompletionCreateRequest(
model: 'deepseek-ai/DeepSeek-V4-Pro',
messages: [ChatMessage.user('What is the capital of Algeria?')],
responseFormat: ResponseFormat.jsonSchema(
name: 'capital',
schema: const {
'type': 'object',
'properties': {
'city': {'type': 'string'},
'country': {'type': 'string'},
},
'required': ['city', 'country'],
'additionalProperties': false,
},
),
),
);
final text = response.text;
if (text == null) {
throw StateError('The response contained no text');
}
return jsonDecode(text) as Map<String, dynamic>;
}It returns Algiers, Algeria.
Embeddings
BAAI/bge-m3 returns one 1024-dimensional vector per input.
/// Returns one vector per input, in input order.
Future<List<List<double>>> embed(OpenAIClient client, List<String> texts) async {
final response = await client.embeddings.create(
EmbeddingRequest(
model: 'BAAI/bge-m3',
input: EmbeddingInput.textList(texts),
),
);
final vectors = List<List<double>>.filled(texts.length, const <double>[]);
for (final item in response.data) {
if (item.index < 0 || item.index >= vectors.length) {
throw StateError('Embedding index out of range');
}
vectors[item.index] = item.embedding;
}
return vectors;
}Handle errors
API errors are typed exceptions, such as AuthenticationException for 401 and RateLimitException for 429. openai_dart retries 429 responses up to 3 times by default and does not retry chat completions on 5xx, so a request is never sent twice.
/// Turns an error into a message you can act on.
/// By default the client retries 429 responses up to 3 times; chat
/// completions (POST) are not retried on 5xx, so a request is never repeated.
String explainError(Object error) {
if (error is AuthenticationException) {
return 'invalid or revoked API key (401)';
}
if (error is RateLimitException) {
return 'rate limit reached, retry later (429)';
}
if (error is ApiException) {
final code = error.statusCode;
final message = error.message;
return 'API error $code: $message';
}
return error.toString();
}Use dart_openai
Already on dart_openai? It adds /v1 to the base URL itself, so give it https://api.devupai.com.
dart pub add dart_openaiimport 'dart:io';
import 'package:dart_openai/dart_openai.dart';/// Creates a dart_openai client for DEVUP AI.
/// dart_openai adds /v1 to the base URL itself, so the base URL has no /v1.
OpenAIClient createDartOpenAIClient() {
final apiKey = Platform.environment['DEVUP_API_KEY'];
if (apiKey == null || apiKey.isEmpty) {
throw StateError('DEVUP_API_KEY is not set');
}
return OpenAIClient(apiKey: apiKey, baseUrl: 'https://api.devupai.com');
}
/// Sends one message and returns the model's reply.
Future<String> chatDartOpenAI(OpenAIClient client) async {
final completion = await client.chat.create(
model: 'deepseek-ai/DeepSeek-V4-Pro',
messages: [
OpenAIChatCompletionChoiceMessageModel.textContent(
role: OpenAIChatMessageRole.user,
text: 'Reply with exactly: Salam DEVUP',
),
],
);
final content = completion.choices.first.message.content ?? const [];
return content.map((item) => item.text ?? '').join();
}Streaming with usage:
/// Prints tokens as they arrive and returns the full text.
Future<String> streamDartOpenAI(OpenAIClient client) async {
final chunks = client.chat.createStream(
model: 'deepseek-ai/DeepSeek-V4-Pro',
messages: [
OpenAIChatCompletionChoiceMessageModel.textContent(
role: OpenAIChatMessageRole.user,
text: 'Count from 1 to 5, separated by spaces.',
),
],
streamOptions: const {'include_usage': true},
);
final text = StringBuffer();
await for (final chunk in chunks) {
for (final choice in chunk.choices) {
for (final item in choice.delta.content ?? const []) {
final piece = item?.text ?? '';
stdout.write(piece);
text.write(piece);
}
}
final usage = chunk.usage;
if (usage != null) {
final promptTokens = usage.promptTokens;
final completionTokens = usage.completionTokens;
print('\ntokens: $promptTokens in, $completionTokens out');
}
}
return text.toString();
}Choosing models
Use exact catalog IDs as the model value: a chat model such as deepseek-ai/DeepSeek-V4-Pro, and BAAI/bge-m3 for embeddings. Tool calling needs a model with tool calling. See Models, Embeddings and Tool Calling.
Troubleshooting
- dart_openai returns 404:The base URL ends in /v1. dart_openai adds /v1 itself, so use https://api.devupai.com.
- dart_openai times out on long answers:Its default request timeout is 30 seconds. Pass a longer requestsTimeOut when you create OpenAIClient.
- The request fails with 401:The key is missing, mistyped or revoked. Check that DEVUP_API_KEY is set where your server runs, or create a new key in the dashboard.
FAQ
Does Flutter work with DEVUP AI?
Yes. Use openai_dart with the base URL https://api.devupai.com/v1, or dart_openai with https://api.devupai.com, on your server, and let your Flutter app call that server.
Should I put my DEVUP AI key in my Flutter app?
No. Anyone can extract a key from an app package. Keep the key on your server and let the app call your server.
How is usage from Dart billed on DEVUP AI?
Every chat and embedding request is a regular DEVUP AI API call, metered in Algerian Dinar on your account.
Which Dart version do I need?
Dart 3.12 or newer for openai_dart 9.
Why does dart_openai need the base URL without /v1?
dart_openai adds /v1 to every request, so https://api.devupai.com becomes https://api.devupai.com/v1.
Can I create embeddings from Dart?
Yes. Call client.embeddings.create with the model BAAI/bge-m3. Each input returns a 1024-dimensional vector.
Related integrations
- Go: DEVUP AI from Go with openai-go or go-openai.
- Node.js SDK: The official DEVUP AI SDK for Node.js and TypeScript.
- Python: DEVUP AI from Python and its frameworks.
- PHP: DEVUP AI from PHP with openai-php/client.