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). system is 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* and chat* build the same wire payload — complete* is just chat* with one user message.
EnvKeyResolver
Reads keys from Platform.environment using 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; only gemini-* ids that support generateContent are 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. system lives on ChatRequest.system, not here — every provider treats system instructions specially (Anthropic puts them at top-level; Gemini uses systemInstruction; OpenAI uses a role:system message). 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 quota in the body) is not retryable — callers should surface that to the user instead of burning attempts.

Functions

decodeSseStream(Stream<List<int>> source) Stream<SseEvent>
Decodes a UTF-8 byte stream into SseEvents. Buffers across chunk boundaries (one event may span several Stream<List<int>> chunks), joins multi-line data: payloads with \n per 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