agentic 1.3.0
agentic: ^1.3.0 copied to clipboard
Ai Wrapper
Agentic #
agentic is a Dart package for building AI-powered apps with one consistent API across multiple providers.
It gives you:
- direct chat calls through a shared request/response model
- built-in model metadata for pricing and capabilities
- a stateful
Agentabstraction for multi-step tool use - structured output via JSON schema
- embeddings on supported OpenAI-compatible backends, including OpenRouter
- an OpenRouter management-key client for API key lifecycle management
- Chunky-backed chunking plus recursive distillation utilities for long-form ingestion
What This Project Is #
Agentic is a lightweight wrapper around several LLM providers that normalizes the parts that are usually annoying to re-implement over and over:
- provider-specific request formatting
- model capability differences
- tool calling
- structured outputs
- token usage and cost tracking
- stateful chat history for agents
Instead of wiring each provider differently, you work with the same ChatRequest, ChatModel, Message, Content, Tool, and Agent types.
What It Supports #
Out of the box, the package includes connectors for:
- OpenAI
- Anthropic
- Google Gemini
- xAI
- Inception Labs
- Ollama
There are also additional OpenAI-compatible connectors in the repo for:
- OpenRouter
- Naga
Those advanced connectors currently require direct imports instead of the main barrel export.
The package also includes a typed OpenRouterManagementClient for managing OpenRouter API keys with a management key.
Features #
- Unified connector interface with
ChatConnector - Built-in model catalog with pricing and capability metadata
- Exact cost tracking using
Rational - Text and image chat inputs through the shared
Contentmodel - Stateful agents with recursive tool execution
- Tool schemas with strict JSON-schema-based arguments
- Structured outputs with
ToolSchema - Embeddings on supported OpenAI-compatible connectors, including OpenRouter
- OpenRouter management-key client for creating, listing, updating, and deleting API keys
- Local model support through Ollama
- Chunky-backed document chunking, file ingestion, rechunking, distillation, and recursive summarization
- Generated codecs for Agentic's own data models via
artifact
Installation #
dart pub add agentic
If you want generated schemas/codecs for your own tool input/output models, also add:
dart pub add artifact
dart pub add dev:build_runner
dart pub add dev:artifact_gen
Then run:
dart run build_runner build --delete-conflicting-outputs
Core Concepts #
ChatConnector: a provider client such asOpenAIConnectororGoogleConnectorChatModel: a model definition with id, pricing, and capabilitiesChatRequest: the normalized request object sent to a connectorMessageandContent: shared chat/message primitivesAgent: a stateful wrapper that reads history, calls the model, runs tools, and appends resultsTool: a callable unit the model can invokeToolSchema: a JSON schema for structured model outputIChunkerandIDistiller: Agentic's compatibility layer for chunking and LLM-backed distillationChunker,FileStringer, andEmbedder: direct Chunky ingestion primitives re-exported by Agentic
Basic Usage #
import 'package:agentic/agentic.dart';
Future<void> main() async {
final openai = OpenAIConnector(apiKey: 'sk-proj-...');
final result = await openai(
ChatRequest(
model: ChatModel.openai4_1Mini,
messages: [
Message.system('You are brief and helpful.'),
Message.user('Give me three weekend side-project ideas.'),
],
),
);
print(result.message.content);
print('Input tokens: ${result.usage.inputTokens}');
print('Output tokens: ${result.usage.outputTokens}');
print('Approx cost: \$${result.realCost.toDouble()}');
}
What this does:
- sends a normalized
ChatRequest - returns an
AgentMessageinsideChatResult - tracks token usage and exact estimated cost
Connector Examples #
import 'package:agentic/agentic.dart';
final openai = OpenAIConnector(apiKey: 'sk-proj-...');
final anthropic = AnthropicConnector(apiKey: 'sk-ant-...');
final google = GoogleConnector(apiKey: '...');
final xai = XaiConnector(apiKey: 'xai-...');
final inception = InceptionLabsConnector(apiKey: '...');
final ollama = OLlamaConnector();
If you want a connected model object for agents, use connect:
final llm = OpenAIConnector(apiKey: 'sk-proj-...')
.connect(ChatModel.openai4_1Mini);
OpenRouter Management Keys #
OpenRouterManagementClient is a separate utility client for OpenRouter's management-key API. You pass it a management key such as sk-management-... and use it to create, list, inspect, update, enable, disable, and delete OpenRouter API keys.
Unlike OpenRouterConnector, this is not a chat connector. It is an HTTP client for key administration.
import 'package:agentic/agentic.dart';
Future<void> main() async {
final client = OpenRouterManagementClient('sk-management-key');
final recurring = await client.createRecurringKey(
name: 'Analytics Service Key',
limit: 100,
reset: OpenRouterApiKeyLimitReset.weekly,
includeByokInLimit: false,
);
print('New key secret: ${recurring.key}');
print('New key hash: ${recurring.data.hash}');
final oneTime = await client.createOneTimeUseBurnKey(
name: 'Temporary Burn Key',
limit: 15,
expiresAt: DateTime.utc(2029, 11, 30, 23, 59, 59),
);
final keys = await client.listApiKeys(includeDisabled: true);
print('Managed key count: ${keys.length}');
final current = await client.getCurrentApiKey();
print('Is management key: ${current.isManagementKey}');
final updated = await client.setRecurringLimit(
recurring.data.hash,
name: 'Updated Analytics Key',
limit: 75,
reset: OpenRouterApiKeyLimitReset.daily,
);
print('Updated limit reset: ${updated.limitReset}');
await client.disableKey(oneTime.data.hash);
await client.removeApiKey(oneTime.data.hash);
client.close();
}
Common methods:
getCurrentApiKey()orgetCurrentKey()listApiKeys()orlistKeys()getApiKey(hash)orgetKey(hash)createApiKey(request)orcreateKey(request)createRecurringKey(...)createOneTimeUseBurnKey(...)updateApiKey(hash, request)orupdateKey(hash, request)setRecurringLimit(...)setOneTimeUseBurnLimit(...)enableKey(hash)disableKey(hash)removeApiKey(hash)ordeleteApiKey(hash)
Request helpers:
OpenRouterCreateApiKeyRequest.recurring(...)creates a key with a recurring budget reset such as daily, weekly, or monthlyOpenRouterCreateApiKeyRequest.oneTimeUseBurn(...)creates a fixed-spend burn key with no recurring resetOpenRouterCreateApiKeyRequest.unlimited(...)creates a key with no spending limitOpenRouterUpdateApiKeyRequest.recurring(...)changes a key to a recurring budgetOpenRouterUpdateApiKeyRequest.oneTimeUseBurn(...)changes a key to a one-time burn budgetOpenRouterUpdateApiKeyRequest.unlimited(...)clears spending limits and recurring resets
Notes:
createRecurringKeyandcreateOneTimeUseBurnKeyreturn anOpenRouterCreatedApiKey, which includes both the created key metadata and the actual secret key string.- The secret key value is only returned at creation time by OpenRouter, so you should store it when you create it.
expiresAtis sent as a UTC ISO 8601 timestamp.limitResetusesOpenRouterApiKeyLimitReset.daily,.weekly, or.monthly.
Stateful Agents #
Agent is the higher-level abstraction in this package. It keeps chat history in a ChatProvider, calls the model, executes tools, appends tool responses, and can continue recursively.
MemoryChatProvider is available, but it currently needs a direct import:
import 'package:agentic/agentic.dart';
import 'package:agentic/chat/agent/chat_provider.dart';
Future<void> main() async {
final agent = Agent(
llm: OpenAIConnector(apiKey: 'sk-proj-...')
.connect(ChatModel.openai4_1Mini),
chatProvider: MemoryChatProvider(
messages: [
Message.system('You are a helpful assistant.'),
Message.user('Explain what this library does in one sentence.'),
],
),
);
final firstReply = await agent();
print(firstReply.content);
await agent.addMessage(
Message.user('Now rewrite that for a README tagline.'),
);
final secondReply = await agent();
print(secondReply.content);
}
Tool Example #
import 'package:agentic/agentic.dart';
import 'package:agentic/chat/agent/chat_provider.dart';
class UppercaseTool extends Tool {
UppercaseTool()
: super(
name: 'uppercase',
description: 'Convert a string to uppercase.',
);
@override
Map<String, dynamic> get schema => {
'type': 'object',
'additionalProperties': false,
'required': ['text'],
'properties': {
'text': {
'type': 'string',
'description': 'The text to convert to uppercase.',
},
},
};
@override
Future<String> call({
required Agent agent,
required Map<String, dynamic> arguments,
}) async {
return (arguments['text'] as String).toUpperCase();
}
}
Future<void> main() async {
final agent = Agent(
llm: OpenAIConnector(apiKey: 'sk-proj-...')
.connect(ChatModel.openai4_1Mini),
chatProvider: MemoryChatProvider(
messages: [
Message.system('Use tools when they help.'),
Message.user('Turn "agentic makes wrappers easier" into uppercase.'),
],
),
);
final reply = await agent(tools: [UppercaseTool()]);
print(reply.content);
}
Structured Output Example #
Use responseFormat when you want a strict JSON object back instead of free-form text.
import 'dart:convert';
import 'package:agentic/agentic.dart';
Future<void> main() async {
final openai = OpenAIConnector(apiKey: 'sk-proj-...');
final result = await openai(
ChatRequest(
model: ChatModel.openai4_1Mini,
messages: [
Message.user('Summarize Agentic in one sentence and give it 3 tags.'),
],
responseFormat: const ToolSchema(
name: 'project_summary',
description: 'A short summary and tags for the project.',
schema: {
'type': 'object',
'additionalProperties': false,
'required': ['summary', 'tags'],
'properties': {
'summary': {'type': 'string'},
'tags': {
'type': 'array',
'items': {'type': 'string'},
},
},
},
),
),
);
final json = jsonDecode(result.message.content.toString())
as Map<String, dynamic>;
print(json['summary']);
print(json['tags']);
}
This is currently a good fit for connectors in this package that support structured output, such as OpenAI and Google.
Multimodal Example #
For multimodal prompts, build a UserMessage manually with grouped content:
import 'package:agentic/agentic.dart';
Future<void> main() async {
final google = GoogleConnector(apiKey: '...');
final result = await google(
ChatRequest(
model: ChatModel.googleGemini2_5Flash,
messages: [
Message.system('Answer in one sentence.'),
UserMessage(
content: Content.group([
Content.text('What is happening in this image?'),
Content.imageUrl('https://example.com/example.png'),
]),
),
],
),
);
print(result.message.content);
}
Local Models With Ollama #
For local or custom OpenAI-compatible models, define the model yourself:
import 'package:agentic/agentic.dart';
Future<void> main() async {
final ollama = OLlamaConnector();
final result = await ollama(
ChatRequest(
model: ChatModel.basic('gpt-oss:20b'),
messages: [
Message.user('Write a haiku about clean APIs.'),
],
),
);
print(result.message.content);
}
Use:
ChatModel.basic(...)when the backend behaves like a modern tool/JSON-capable modelChatModel.safe(...)when you want a conservative fallback with fewer assumptions
Embeddings Example #
OpenAI-compatible connectors that implement embeddings expose embed and embedMultiple.
import 'package:agentic/agentic.dart';
Future<void> main() async {
final openai = OpenAIConnector(apiKey: 'sk-proj-...');
final vector = await openai.embed(
model: 'text-embedding-3-small',
text: 'Agentic wraps multiple providers behind one Dart API.',
dimensions: 256,
);
print('Embedding dimensions: ${vector.length}');
}
OpenRouter embeddings are also supported through OpenRouterConnector, which currently needs a direct import:
import 'package:agentic/chat/connector/connector_openrouter.dart';
Future<void> main() async {
final openrouter = OpenRouterConnector(apiKey: 'sk-or-v1-...');
final vector = await openrouter.embed(
model: 'perplexity/pplx-embed-v1-4b',
text: 'Agentic wraps multiple providers behind one Dart API.',
dimensions: 256,
);
print('OpenRouter embedding dimensions: ${vector.length}');
}
Chunking And Distillation #
Agentic now uses Chunky as its ingestion backend. That gives you two supported paths:
- stay on
IChunkerwhen you want Agentic's existing chunk metadata and recursive distillation workflow - use Chunky's
Chunker,FileStringer, andEmbedderdirectly when you want lower-level ingestion helpers
IDistiller currently needs a direct import:
import 'package:agentic/agentic.dart';
import 'package:agentic/ingest/distiller.dart';
const longDocumentText = '''
Put a long body of text here.
It can be OCR output, a transcript, notes, or raw document text.
''';
Future<void> main() async {
final chunker = IChunker(maxChunkSize: 500, maxPostOverlap: 100);
final distiller = IDistiller(
llm: OpenAIConnector(apiKey: 'sk-proj-...')
.connect(ChatModel.openai4_1Mini),
targetOutputSize: 400,
);
await for (final chunk in chunker.recursiveDistillChunks(
chunks: chunker.chunkString(longDocumentText),
distiller: distiller,
factor: 4,
parallelism: 4,
)) {
print('L${chunk.lod} #${chunk.index}: ${chunk.fullContent}');
}
}
This is useful for:
- hierarchical summarization
- document ingestion pipelines
- cleaning up noisy OCR text
- compressing large inputs before later prompts
If you want to work with the underlying Chunky primitives directly, they are now re-exported from package:agentic/agentic.dart:
import 'dart:io';
import 'package:agentic/agentic.dart';
Future<void> main() async {
final chunker = Chunker(chunkSize: 400);
await for (final chunk in chunker.transformString(
'Chunky is now the ingestion backend behind Agentic.',
)) {
print('#${chunk.id} @ ${chunk.start}: ${chunk.content}');
}
await for (final chunk in chunker.transformFile(File('notes.txt'))) {
print('File chunk ${chunk.id}: ${chunk.content}');
}
final embedder = Embedder(
chunker: chunker,
overlap: 80,
embedder: (text) async => List<double>.filled(8, text.length.toDouble()),
);
await for (final embedded in embedder.transform(
Stream.value('Embed long-form content in one pass.'),
)) {
print(
'Embedded chunk ${embedded.chunk.id}: ${embedded.embedding.length} dims',
);
}
}
Advanced Imports #
The main barrel export is:
import 'package:agentic/agentic.dart';
A few useful pieces currently live behind direct imports:
import 'package:agentic/chat/agent/chat_provider.dart';
import 'package:agentic/chat/connector/connector_naga.dart';
import 'package:agentic/chat/connector/connector_openrouter.dart';
import 'package:agentic/ingest/distiller.dart';
import 'package:agentic/util/naga_models.dart';
import 'package:agentic/util/open_router_models.dart';
That gives you access to:
MemoryChatProviderNagaConnectorOpenRouterConnectorIDistiller- live or cached model catalogs for Naga and OpenRouter
Notes And Limitations #
AnthropicConnectorin this package does not currently supportresponseFormat; it throws if structured output is requested.OpenRouterConnectorsupports embeddings through the sameembedandembedMultipleinterface as other OpenAI-compatible connectors.OpenRouterManagementClientis for management-key administration, not chat completions.- Agentic re-exports Chunky, so
Chunker,FileStringer, andEmbedderare available from the main barrel import. - Audio content types exist in the shared content model, but the current LangChain bridge only wires text and images for chat requests.
OpenRouterConnectorandNagaConnectorare present in the repo, but they are not exported from the mainagentic.dartbarrel yet.
Why Use It #
Agentic is a good fit if you want a small Dart-first library that lets you:
- swap providers without rewriting your application code
- build tool-using agents without wiring a loop from scratch
- keep model metadata and cost tracking close to your call site
- use local and hosted models through the same abstractions
- add document chunking, file ingestion, and distillation without stitching two packages together yourself
If you want one package that covers direct prompting, agents, tools, structured output, cost tracking, and ingestion utilities, that is what this repo is built for.