A lightweight, transport-agnostic library for integrating 20 LLM engines into your applications. Use it in agents, web apps, CLI tools, or any Node.js project that needs multi-provider LLM access.
@abbenay/core is the reusable library extracted from the Abbenay daemon. It provides:
- Multi-engine abstraction over 20 LLM providers via the Vercel AI SDK 7 (requires Node.js 22.12+)
- Streaming chat with tool calling; optional
reasoningeffort pass-through to the model (reasoning deltas are not streamed to clients yet); flattimeoutms maps to AI SDK{ totalMs } - Model discovery from provider APIs
- YAML configuration management (user-level and workspace-level)
- Pluggable secret store for API key management
It has zero transport dependencies — no gRPC, no Express, no CLI. Just the LLM logic.
Download the abbenay-core-*.tgz tarball from the
latest GitHub release
and install it locally:
npm install abbenay-core-<version>.tgzThen install only the AI SDK provider packages you need:
npm install @ai-sdk/openai # for OpenAI
npm install @ai-sdk/anthropic # for Anthropic
npm install @ai-sdk/google # for Google Gemini
npm install @ai-sdk/google-vertex # for Vertex AI (Anthropic Claude on Vertex)
npm install @ai-sdk/openai-compatible # for Ollama, OpenRouter, LM Studio, etc.Provider packages are optional peer dependencies — loaded dynamically at runtime only when you use that engine. If a package is missing, you get a clear error message telling you what to install.
Build providers entirely in memory — no YAML, no disk:
import { CoreState, MemorySecretStore } from '@abbenay/core';
const core = new CoreState({ secretStore: new MemorySecretStore() });
// Add a provider with an API key — stored in-memory, ready immediately
await core.addProvider('my-openai', {
engine: 'openai',
apiKey: process.env.OPENAI_API_KEY!,
models: { 'gpt-4o': {}, 'gpt-4o-mini': { temperature: 0.3 } },
});
// Stream a chat response
for await (const chunk of core.chat('my-openai/gpt-4o', [
{ role: 'user', content: 'Hello!' },
])) {
if (chunk.type === 'text') process.stdout.write(chunk.text);
if (chunk.type === 'done') console.log('\n[done]');
}If you have a ~/.config/abbenay/config.yaml, CoreState reads it automatically:
const core = new CoreState({ secretStore: new MemorySecretStore() });
// Reads providers from disk config
const providers = await core.listProviders();
const models = await core.listModels();You can also mix both — disk config plus programmatic additions. In-memory providers take precedence.
@abbenay/core uses the same YAML config files as the full Abbenay daemon.
- User level:
~/.config/abbenay/config.yaml - Workspace level:
<workspace>/.config/abbenay/config.yaml
providers:
my-openai:
engine: openai
api_key_keychain_name: "OPENAI_API_KEY"
models:
gpt-4o: {}
gpt-4o-mini:
temperature: 0.3
max_tokens: 4096
local-ollama:
engine: ollama
base_url: "http://127.0.0.1:11434/v1"
models:
llama3.2: {}
qwen2.5-coder:
model_id: "qwen2.5-coder:7b"Key concepts:
- Virtual provider — a user-defined name (e.g.,
my-openai) that maps to an engine - Engine — the actual API backend (
openai,anthropic,ollama, etc.) - Virtual model — a user-defined name within a provider; maps to an engine model ID
- Composite ID —
{provider}/{model}(e.g.,my-openai/gpt-4o) used inchat()calls
import { loadConfig, saveConfig, getUserConfigPath } from '@abbenay/core';
const config = loadConfig();
console.log(config.providers);
// Modify and save
config.providers!['my-openai'].models!['gpt-4o'] = { temperature: 0.7 };
saveConfig(config);ConfigFileSchema and PolicyConfigSchema (plus parseConfigFile) validate
config-shaped objects before persistence. The daemon HTTP API uses the same
schemas for POST /api/config and related routes.
import { parseConfigFile } from '@abbenay/core';
const result = parseConfigFile(raw);
if (!result.success) {
console.error(result.error.issues);
}The main entry point. Manages providers, models, and chat.
import { CoreState, MemorySecretStore } from '@abbenay/core';
const core = new CoreState({
secretStore: new MemorySecretStore(), // or your own SecretStore implementation
configLoader: () => myCustomConfig, // optional: override config loading
});| Method | Returns | Description |
|---|---|---|
addProvider(id, options) |
Promise<void> |
Add a provider programmatically |
removeProvider(id) |
boolean |
Remove an in-memory provider |
addModel(providerId, modelName, config?) |
void |
Add a model to an existing provider |
removeModel(providerId, modelName) |
boolean |
Remove a model from a provider |
hasProvider(id) |
boolean |
Check if a provider exists |
| Method | Returns | Description |
|---|---|---|
listProviders(workspacePaths?) |
Promise<ProviderInfo[]> |
List all virtual providers with config status |
listModels(workspacePaths?) |
Promise<ModelInfo[]> |
List all virtual models across providers |
listEngines() |
EngineInfo[] |
List available engine types (the fixed set) |
discoverModels(engineId, apiKey?, baseUrl?) |
Promise<DiscoveredModel[]> |
Fetch models from an engine's API |
resolveApiKey(providerId, providerCfg?) |
Promise<string | null> |
Resolve API key from keychain or env var |
chat(compositeModelId, messages, params?, toolOptions?, toolExecutor?) |
AsyncGenerator<ChatChunk> |
Stream a chat response |
runHealthChecks() |
Promise<void> |
Run background health checks |
Build providers on the fly without config files:
// Add a provider with API key (stored in the injected SecretStore)
await core.addProvider('my-anthropic', {
engine: 'anthropic',
apiKey: 'sk-ant-...',
models: {
'claude-sonnet-4-20250514': {},
'claude-3-5-haiku-20241022': { temperature: 0.7 },
},
});
// Or use an environment variable instead of a raw key
await core.addProvider('work-openai', {
engine: 'openai',
apiKeyEnvVar: 'WORK_OPENAI_KEY',
baseUrl: 'https://corp-proxy.example.com/v1',
models: { 'gpt-4o': {} },
});
// Add a model to an existing provider later
core.addModel('my-anthropic', 'claude-3-opus-20240229', { max_tokens: 8192 });
// Remove a model
core.removeModel('my-anthropic', 'claude-3-5-haiku-20241022');
// Remove an entire provider
core.removeProvider('work-openai');
// Check if a provider exists (in-memory or disk config)
if (core.hasProvider('my-anthropic')) { ... }AddProviderOptions:
| Field | Type | Description |
|---|---|---|
engine |
string |
Required. Engine type ("openai", "anthropic", etc.) |
apiKey |
string |
API key value — stored in SecretStore automatically |
apiKeyEnvVar |
string |
Environment variable name (alternative to apiKey) |
baseUrl |
string |
Custom base URL (overrides engine default); validated per DR-040 endpoint policy |
models |
Record<string, ModelConfig> |
Models to enable |
In-memory providers merge over disk config. If both define the same provider ID, the in-memory version wins.
Interface for API key storage. Implement your own or use the built-in MemorySecretStore.
interface SecretStore {
get(key: string): Promise<string | null>;
set(key: string, value: string): Promise<void>;
delete(key: string): Promise<boolean>;
has(key: string): Promise<boolean>;
}The chat() method yields ChatChunk objects:
type ChatChunk =
| { type: 'text'; text: string } // LLM text output
| { type: 'tool'; name: string; state: string; ... } // Tool call/result
| { type: 'error'; error: string } // Error
| { type: 'done'; finishReason: string } // Stream finishedPass tool definitions and an executor to chat():
const tools = [{
name: 'get_weather',
description: 'Get current weather for a city',
inputSchema: JSON.stringify({
type: 'object',
properties: { city: { type: 'string' } },
required: ['city'],
}),
}];
const executor = async (toolName: string, args: Record<string, any>) => {
if (toolName === 'get_weather') {
return { temperature: 72, condition: 'sunny' };
}
};
for await (const chunk of core.chat(
'my-openai/gpt-4o',
[{ role: 'user', content: 'What is the weather in London?' }],
undefined, // params
{ tools }, // toolOptions
executor, // toolExecutor
)) {
console.log(chunk);
}The Vercel AI SDK (v7) handles the tool execution loop automatically via
stopWhen: isStepCount(...) (default max 10 iterations, configurable through
tool_policy.max_tool_iterations). Tool approval uses SDK-native toolApproval
bridged to Abbenay's secure-by-default policy (DR-019 / DR-042).
| Engine | ID | Requires Key | SDK Package | Notes |
|---|---|---|---|---|
| OpenAI | openai |
Yes | @ai-sdk/openai |
GPT-4o, o1, etc. |
| Anthropic | anthropic |
Yes | @ai-sdk/anthropic |
Claude 4, 3.5, etc. |
| Google Gemini | gemini |
Yes | @ai-sdk/google |
Gemini Pro, Flash |
| Mistral | mistral |
Yes | @ai-sdk/mistral |
Mistral Large, etc. |
| xAI (Grok) | xai |
Yes | @ai-sdk/xai |
Grok models |
| DeepSeek | deepseek |
Yes | @ai-sdk/deepseek |
DeepSeek Coder |
| Groq | groq |
Yes | @ai-sdk/groq |
Fast inference |
| Cohere | cohere |
Yes | @ai-sdk/cohere |
Command models |
| Amazon Bedrock | bedrock |
No* | @ai-sdk/amazon-bedrock |
Uses AWS credentials |
| Vertex Anthropic | vertex-anthropic |
No* | @ai-sdk/google-vertex |
Claude on Vertex AI; supports Bearer-token proxies |
| Fireworks | fireworks |
Yes | @ai-sdk/fireworks |
Optimized inference |
| Together AI | togetherai |
Yes | @ai-sdk/togetherai |
Open models |
| Perplexity | perplexity |
Yes | @ai-sdk/perplexity |
Search-augmented |
| Azure OpenAI | azure |
Yes | @ai-sdk/openai-compatible |
Custom base URL required |
| OpenRouter | openrouter |
Yes | @ai-sdk/openai-compatible |
100+ model aggregator |
| Ollama | ollama |
No | @ai-sdk/openai-compatible |
Local models |
| LM Studio | lmstudio |
No | @ai-sdk/openai-compatible |
Local models |
| Cerebras | cerebras |
Yes | @ai-sdk/openai-compatible |
Fast inference |
| Meta (Llama) | meta |
Yes | @ai-sdk/openai-compatible |
Llama API |
| Red Hat AI | redhat |
No | @ai-sdk/openai-compatible |
Red Hat AI Inference or OpenShift AI MaaS |
| Mock | mock |
No | (built-in) | Testing only |
Policies are named behavioral presets that can be assigned to models. A model references a policy by name; the policy's fields act as defaults that can be overridden by explicit model config or request parameters.
| Policy | Purpose |
|---|---|
precise |
Low temperature (0.15), concise factual responses |
balanced |
General-purpose (temp 0.5, 4096 tokens) |
creative |
High temperature (0.9), longer output |
coder |
Low temperature, complete runnable code |
json_strict |
JSON-only output with retry on invalid JSON |
long_context_chat |
Concise follow-ups in long conversations |
Define custom policies in ~/.config/abbenay/policies.yaml:
my-policy:
sampling:
temperature: 0.3
top_p: 0.8
output:
max_tokens: 4096
system_prompt_snippet: "Be concise."
reliability:
timeout: 30000providers:
my-openai:
engine: openai
models:
gpt-4o:
policy: coder
temperature: 0.1 # Overrides the policy's temperatureimport { resolvePolicy, listAllPolicies, flattenPolicy } from '@abbenay/core';
const policy = resolvePolicy('coder');
const all = listAllPolicies(); // Built-in + custom
const flat = flattenPolicy(policy!); // { params, systemPromptSnippet, ... }The ToolRegistry collects tools from multiple sources, namespaces them, and builds executors compatible with CoreState.chat().
import { ToolRegistry } from '@abbenay/core';
const registry = new ToolRegistry();
// Register local tools with inline executors
registry.register('myAgent', 'local', [
{
name: 'search',
description: 'Search documentation',
inputSchema: JSON.stringify({ type: 'object', properties: { query: { type: 'string' } } }),
executor: async (args) => ({ results: ['...'] }),
},
]);const tools = registry.listForChat();
const executor = registry.buildExecutor();
for await (const chunk of core.chat('my-openai/gpt-4o', messages, undefined, { tools }, executor)) {
console.log(chunk);
}Control tool visibility and approval via ToolPolicyConfig. Chat and the
daemon MCP HTTP server share createToolValidator / authorizeToolExecution
from tool-approval.ts (DR-033):
import { createToolValidator, authorizeToolExecution } from '@abbenay/core';
const tools = registry.listForChat({
disabled_tools: ['mcp:filesystem/*'], // Never send to LLM
auto_approve: ['local:*/*'], // Execute without confirmation
require_approval: ['ws:*/*'], // Pause and ask user
});
// Same precedence for any execution surface:
// disabled → deny; require_approval → ask; auto_approve → allow; default → ask
const validator = createToolValidator(registry, policy, onApprovalNeeded);@abbenay/core is the library. @abbenay/daemon is the full application built on top of it.
@abbenay/core (this package)
CoreState, engines, config, secrets interface
Zero transport dependencies
For: agent devs, web devs, custom apps
@abbenay/daemon (full application)
DaemonState extends CoreState
+ gRPC server, web UI, CLI, VS Code backchannel
+ KeychainSecretStore (keytar), MCP, SEA binary
For: end users running the daemon
Both are built from a single source tree (packages/daemon/src/). The core code lives in src/core/, the daemon-specific code in src/daemon/.