ai_broker library
One Dart surface over Claude / OpenAI / Gemini. Apps depend on AiBroker (not a concrete provider) so swapping models is a registry lookup, not a code change.
Classes
- AiBroker
- Vendor-neutral AI broker. One implementation per provider (Claude, OpenAI, Gemini). Callers depend on this interface; the AiBrokerRegistry is the indirection that lets the active provider change at runtime.
- AiBrokerRegistry
- Process-wide registry. Wire brokers once at startup, then look them up by AiBroker.id from anywhere in the app.
- AiMessage
- AnthropicBroker
-
Anthropic Claude (
/v1/messages).systemis a top-level field, not a role — keep it out of ChatRequest.messages. - ChatRequest
-
What every broker call boils down to. system is the persistent
instruction; messages is the turn history (oldest first). Both
complete*andchat*build the same wire payload —complete*is justchat*with one user message. - EnvKeyResolver
-
Reads keys from
Platform.environmentusing a per-broker env var name. Defaults follow the SDK conventions each provider documents: - GeminiBroker
-
Google Gemini (
generateContent/streamGenerateContent). Pagination on listModels caps at 5 pages × 50; onlygemini-*ids that supportgenerateContentare kept. - KeyResolver
- Where API keys come from. Flutter apps with secure storage implement this against their own store; CLI / backend code can use the built-in EnvKeyResolver or MapKeyResolver.
- MapKeyResolver
- In-memory resolver — for tests, or for apps that already hold keys in a runtime-built map.
- OpenAiBroker
-
OpenAI chat-completions. Filters listModels to
gpt-*/o<digit>*so the picker doesn't show whisper / embeddings / dall-e. - SseEvent
- One parsed Server-Sent Event. OpenAI and Anthropic both use SSE for their streaming endpoints (Gemini uses a JSON-array stream — see the Gemini broker for that codepath).
Enums
- AiRole
-
Who authored a message in a chat.
systemlives on ChatRequest.system, not here — every provider treats system instructions specially (Anthropic puts them at top-level; Gemini usessystemInstruction; OpenAI uses arole:systemmessage). We only model the back-and-forth.
Constants
-
retryableStatusCodes
→ const Set<
int> -
Status codes that mean "try again later, the request itself is
fine": rate limits and transient upstream overload. Quota-exceeded
(a hard 429 with
quotain the body) is not retryable — callers should surface that to the user instead of burning attempts.
Functions
-
decodeSseStream(
Stream< List< source) → Stream<int> >SseEvent> -
Decodes a UTF-8 byte stream into SseEvents. Buffers across chunk
boundaries (one event may span several
Stream<List<int>>chunks), joins multi-linedata:payloads with\nper the SSE spec, and filters out keep-alive comments. -
openSsePost(
{required Uri uri, required Map< String, String> headers, required String body, required String providerLabel, Client? client}) → Future<Stream< List< >int> > - Opens a POST request and returns the response body as a byte stream. Throws AiBrokerException on non-2xx (the body is buffered in that path so we can include the error message).
-
retryRequest(
{required Future< Response> send(), required String providerLabel, int maxAttempts = 5, Duration baseDelay = const Duration(seconds: 5), bool isHardFailure(Response)?}) → Future<Response> - HTTP retry with exponential backoff capped at 5 attempts × 5s × 2ⁿ. Shared by every broker so retry policy lives in one place.
-
stripCodeFence(
String text) → String -
Strips a leading and trailing markdown code fence from
text. Model output for "give me code" prompts usually arrives wrapped in```lang\n...\n```, even after an explicit "code only" instruction. Call this on the broker's raw text before writing it to a.dart/.ts/ etc. file.
Exceptions / Errors
- AiBrokerException
- Thrown for any broker-layer failure that callers can present to the user — auth, rate-limit-after-retries, empty response, malformed response.
- MissingKeyException