ai_box 0.3.0
ai_box: ^0.3.0 copied to clipboard
A base class for all AI providers.
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.