ai_box
Version: V1
This package provides a base class and test utilities for all AI provider classes that inherit from LLMAIBase.
LLMAIBase
An abstract class that all AI providers (such as ChatGPT, Claude, Gemini, etc.) should inherit from.
Required Methods
completions(): Chat completiongetModels(): Get a list of available modelsvalidateKey(): Validate the API key
Provided Utility Methods
chat()/chatStream(): Chat with structured messageschatWithStrings(): Chat with an array of stringsgenerateText(): Simple text generationgenerateTextStream(): Stream text outputaskWithImages(): One-shot prompt with images
Multimodal input
Pass images, audio, or files as LLMContentParts on any message. The same
abstraction works across every provider — the provider converts it to the
right wire format (OpenAI content arrays, Claude content blocks, Gemini
inlineData / fileData).
import 'dart:typed_data';
import 'package:ai_box/ai_box.dart';
final res = await ai.chat(
model: 'gpt-5.5',
messages: [
LLMContent.user('Describe these', attachments: [
LLMImagePart.bytes(pngBytes, mimeType: 'image/png'),
LLMImagePart.url('https://example.com/photo.jpg'),
LLMFilePart.bytes(pdfBytes, mimeType: 'application/pdf'),
]),
],
);
print(res.text);
Just drop in a File (dart:io)
import 'package:ai_box/io.dart';
final msg = LLMContent.user('What is in these?', attachments: [
File('photo.png').toImagePart(),
File('report.pdf').toFilePart(),
File('memo.mp3').toAudioPart(),
]);
ai_box core stays platform-agnostic (no dart:io); the File helpers live
in the separate package:ai_box/io.dart import.
Output: sealed parts cover every case
LLMContent.parts is a list of the sealed LLMContentPart, so a switch
is exhaustive:
for (final part in res.content.parts) {
switch (part) {
case LLMTextPart(:final text): print(text);
case LLMImagePart(): saveImage(part.bytes); // generated image bytes
case LLMAudioPart(): saveAudio(part.bytes); // generated audio bytes
case LLMFilePart(): saveFile(part.bytes);
case LLMToolCallPart(:final name): callTool(name, part.arguments);
case LLMToolResultPart(): break;
case LLMReasoningPart(:final text): logThinking(text);
case LLMCodeExecutionPart(): break;
case LLMCodeExecutionResultPart(): break;
}
}
Convenience getters: res.text, res.content.images, res.content.audioList,
res.content.files, res.toolCalls, res.content.reasoning.
res.finishReason is the normalized LLMFinishReason enum
(stop / length / toolCalls / contentFilter / other), and
res.usage carries inputTokens / outputTokens / cachedInputTokens /
reasoningTokens.
Error handling: sealed LLMException
Every provider normalizes failures into a sealed LLMException:
try {
await ai.generateText(model: '...', message: 'hi');
} on LLMException catch (e) {
switch (e) {
case LLMAuthException(): // 401 / 403
case LLMRateLimitException(): // 429
case LLMInvalidRequestException(): // 4xx
case LLMServerException(): // 5xx
case LLMNetworkException(): // connectivity
case LLMUnknownException():
}
}
Tool / function calling
final res = await ai.chat(
model: 'gpt-5.5',
messages: [LLMContent.user('Weather in Tokyo?')],
tools: [
LLMTool(
name: 'get_weather',
description: 'Get current weather',
parameters: {
'type': 'object',
'properties': {'city': {'type': 'string'}},
'required': ['city'],
},
),
],
toolChoice: LLMToolChoice.auto,
);
for (final call in res.toolCalls) {
final result = await runTool(call.name, call.arguments);
// Send the result back on the next turn:
// LLMContent(role: LLMRole.user, parts: [
// LLMToolResultPart(toolCallId: call.id, content: result),
// ])
}
Structured output (JSON Schema)
final res = await ai.chat(
model: 'gpt-5.5',
messages: [LLMContent.user('Extract name and age')],
responseFormat: LLMResponseFormat.jsonSchema(
schema: {
'type': 'object',
'properties': {
'name': {'type': 'string'},
'age': {'type': 'integer'},
},
'required': ['name', 'age'],
},
),
);
Streaming
Every provider implements true token-by-token SSE streaming:
await for (final text in ai.generateTextStream(model: '...', message: 'Hi')) {
stdout.write(text);
}
For full control (reasoning deltas, tool calls, usage), use chatStream() /
completionsStream():
await for (final chunk in ai.chatStream(
model: '...',
messages: [LLMContent.user('Hi')],
)) {
stdout.write(chunk.delta); // incremental text
logThinking(chunk.reasoningDelta); // incremental thinking (if any)
if (chunk.isDone) {
// The final chunk carries the complete parts, finish reason and usage.
print(chunk.finishReason);
print(chunk.usage);
}
}
Adding a New AI Provider
- Create a class that inherits from
LLMAIBase. - Implement
completions(),getModels()andvalidateKey(). - Optionally override
completionsStream()with the provider's SSE streaming (the shared helpers below already cover OpenAI-compatible APIs).
Shared helpers for provider implementations
Two opt-in libraries keep provider packages free of copy-pasted plumbing
(they are not exported from package:ai_box/ai_box.dart):
package:ai_box/provider_http.dart—requestJson()runs an HTTP call and normalizes failures into the sealedLLMExceptionhierarchy (LLMNetworkExceptionon connectivity errors,LLMException.fromHttpon non-2xx responses).postSseData()/decodeSseJson()do the same for SSE streaming endpoints.package:ai_box/openai_compat.dart—buildOpenAiBody()/parseOpenAiResponse()/postOpenAiJson()/streamOpenAiCompletions()implement the OpenAI-compatible Chat Completions wire format (multimodal content, tools, structured output, SSE streaming), shared by chatgpt_box, deepseek_box, grok_box, minimax_box and openrouter_box.
Libraries
- ai_box
- io
- dart:io 連携ライブラリ。
- openai_compat
- OpenAI 互換 Chat Completions API のリクエスト構築・レスポンス解析ヘルパー。
- provider_http
- プロバイダー実装用の HTTP ヘルパー。