dart_openai
Unofficial Dart/Flutter SDK for the OpenAI API. Typed clients for every major API surface: Responses, Chat Completions, Realtime, Videos, Batch, Fine-tuning, Vector Stores, Evals, Administration and more. Compiles and runs on every platform: Android, iOS, macOS, Linux, Windows, web, and server-side Dart.
Maintained by Anas Fikhi (@anasfik).
Installation
dependencies:
dart_openai: ^8.0.0
dart pub get
Quickstart
import 'package:dart_openai/dart_openai.dart';
Future<void> main() async {
final client = OpenAIClient(apiKey: Platform.environment['OPENAI_API_KEY']!);
final completion = await client.chat.create(
model: 'gpt-4o',
messages: [
const OpenAIChatCompletionChoiceMessageModel(
role: OpenAIChatMessageRole.user,
content: 'Say hello in five words.',
),
],
);
print(completion.choices.first.message.content);
}
Prefer the global facade? It still works:
OpenAI.apiKey = 'sk-...';
await OpenAI.instance.chat.create(model: 'gpt-4o', messages: [...]);
Multiple clients and compatible providers
Every OpenAIClient owns its configuration. Run several accounts, Azure resources, or OpenAI-compatible providers side by side without global state:
final production = OpenAIClient(apiKey: 'sk-prod');
final deepseek = OpenAIClient(
apiKey: 'ds-...',
baseUrl: 'https://api.deepseek.com',
);
final localLlama = OpenAIClient(
apiKey: 'not-needed',
baseUrl: 'http://localhost:1234/v1',
);
Works with any provider that implements the OpenAI wire format: DeepSeek, LM Studio, Ollama, Groq, Together, Azure OpenAI gateways, and others.
Streaming
One SSE engine backs every streaming endpoint. It closes on [DONE], never duplicates events, and surfaces errors as exceptions instead of swallowing them.
Responses API events:
final events = client.responses.createStream(
model: 'gpt-4o',
input: 'Write a haiku about databases.',
);
await for (final event in events) {
if (event['type'] == 'response.output_text.delta') {
stdout.write(event['delta']);
}
}
Chat Completions deltas:
final chunks = client.chat.createStream(model: 'gpt-4o', messages: messages);
await for (final chunk in chunks) {
stdout.write(chunk.choices.first.delta?.content);
}
Images emit partial results as they render (createStream), and stored completions support retrieval and listing.
API coverage
| API | Accessor |
|---|---|
| Responses (incl. streaming, compact) | client.responses |
| Chat Completions (tools, vision, reasoning params, stored completions) | client.chat |
| Conversations | client.conversations |
| Audio (speech, transcription, translation, voices, consents) | client.audio |
| Images (generation, edit, variation, streaming partials) | client.image |
| Embeddings | client.embedding |
| Files (incl. byte uploads) | client.file |
| Uploads (multipart sessions) | client.uploads |
| Batch | client.batch |
| Vector Stores (+ files, + file batches) | client.vectorStores |
| Containers (+ files) | client.container |
| Models / Moderation | client.model / client.moderation |
| Evals | client.evals |
| Graders (incl. run and validate) | client.graders |
| Fine-tuning jobs (current API) | client.fineTuning |
| Videos (generate, remix, download) | client.videos |
| Realtime sessions and client secrets | client.realtime |
| Skills | client.skills |
| Content provenance checks | client.provenance |
| Administration (projects, users, invites, audit logs, costs, rate limits, API keys) | client.organization |
| Completions and Edits (legacy) | client.completion / client.edit |
Not planned: Assistants v1 and Threads (superseded by the Responses API), ChatKit.
Resilience
Requests retry automatically on transient failures (connection errors, HTTP 408/429/5xx). GET requests always retry; POST retries only on rate limits and server errors. Backoff is exponential with jitter and the server's Retry-After header takes precedence.
// Default policy: 2 total attempts.
final client = OpenAIClient(apiKey: 'sk-...');
// Custom:
final aggressive = OpenAIClient(
apiKey: 'sk-...',
retryPolicy: const OpenAIRetryPolicy(maxAttempts: 4),
);
Streaming has an idle watchdog: a connection that stops delivering bytes fails with StreamTimedOutException after the request timeout instead of hanging forever. After any call, quota is observable:
await client.model.list();
print(OpenAIResponseMeta.lastRateLimit?.remainingRequests);
Azure OpenAI
final azure = OpenAIClient(
apiKey: '<azure-key>',
azure: const OpenAIAzure(
resource: 'my-resource',
apiVersion: '2024-10-21',
deployments: {'gpt-4o': 'gpt4o-prod'},
),
);
// model is rewritten to your deployment automatically:
await azure.chat.create(model: 'gpt-4o', messages: messages);
Files on every platform
Upload APIs take a platform-neutral file type — no dart:io, so the same code runs on web:
// From disk (native):
final file = await loadOpenAIFile('training.jsonl');
await client.file.upload(file: file, purpose: 'fine-tune');
// From memory (anywhere, including web):
await client.file.uploadBytes(
bytes: utf8.encode(jsonl),
fileName: 'training.jsonl',
purpose: 'fine-tune',
);
Error handling
All failures throw typed exceptions you can catch precisely:
try {
await client.chat.create(model: 'gpt-4o', messages: messages);
} on RequestFailedException catch (e) {
// Non-2xx from the API: e.message, e.statusCode
} on MissingApiKeyException catch (e) {
// No key configured for this client
} on OpenAIUnexpectedException catch (e) {
// Malformed response that is not an API error payload
}
Error payloads are normalized across providers. Whether a provider returns {"error": {"message": ...}}, a bare string error, or an HTML error page, you get a RequestFailedException with the body preserved.
Configuration
Per-client options: apiKey, organization, baseUrl, version, requestsTimeOut, extraHeaders.
Global facade equivalents: OpenAI.apiKey, OpenAI.organization, OpenAI.baseUrl, OpenAI.requestsTimeOut, plus OpenAI.showLogs and OpenAI.showResponsesLogs for request debugging.
Reasoning models (o-series, GPT-5 family): pass reasoningEffort: 'low' and use maxTokens, which maps to max_completion_tokens. Incompatible sampling parameters are rejected by the API; use extraParams to pass anything not yet modeled.
Migrating from 6.x / 7.x
Nothing breaks if you used the global facade. To adopt per-client instances, replace global configuration with construction:
// Before (still supported)
OpenAI.apiKey = 'sk-...';
await OpenAI.instance.chat.create(...);
// After
final client = OpenAIClient(apiKey: 'sk-...');
await client.chat.create(...);
8.0.0 replaces dart:io File parameters with the platform-neutral OpenAIFile type (await loadOpenAIFile(path) on native, OpenAIFile(bytes:, fileName:) anywhere), and audio.createSpeech now returns bytes.
Full list of changes: CHANGELOG.md.
Testing
The suite runs entirely against mocked HTTP — request shapes, SSE decoding, malformed-payload fuzzing, retry behavior. No API keys required:
dart test
Live integration tests run only when OPEN_AI_API_KEY is present in the environment.
Contributing
Bug reports and feature requests go through GitHub Issues. Pull requests welcome: keep diffs focused, add or update tests for changed behavior, and follow the existing module layout (lib/src/instance/<module>/ with models under lib/src/core/models/<module>/). Commit messages use Conventional Commits.
License
MIT. See LICENSE.
Support
- Documentation: pub.dev/documentation/dart_openai
- Issues: github.com/anasfik/openai/issues
- Sponsor: github.com/sponsors/anasfik