Integrations
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.
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.
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"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.
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();
}
}/// 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.
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.
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.
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;
}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();
}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();
}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.
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.
No. Anyone can extract a key from an app package. Keep the key on your server and let the app call your server.
Every chat and embedding request is a regular DEVUP AI API call, metered in Algerian Dinar on your account.
Dart 3.12 or newer for openai_dart 9.
dart_openai adds /v1 to every request, so https://api.devupai.com becomes https://api.devupai.com/v1.
Yes. Call client.embeddings.create with the model BAAI/bge-m3. Each input returns a 1024-dimensional vector.