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).
dependencies:
dart_openai: ^8.0.0dart pub getimport '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: [...]);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.
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 | 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.
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);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);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',
);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.
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.
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.
The suite runs entirely against mocked HTTP — request shapes, SSE decoding, malformed-payload fuzzing, retry behavior. No API keys required:
dart testLive integration tests run only when OPEN_AI_API_KEY is present in the environment.
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.
MIT. See LICENSE.
- Documentation: pub.dev/documentation/dart_openai
- Issues: github.com/anasfik/openai/issues
- Sponsor: github.com/sponsors/anasfik