@abbenay/core

July 7, 2026 · View on GitHub

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.

What is it?

@abbenay/core is the reusable library extracted from the Abbenay daemon. It provides:

  • Multi-engine abstraction over 15+ LLM providers via the Vercel AI SDK
  • Streaming chat with tool calling support
  • 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.

Install

npm install @abbenay/core

Then 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.

Quick Start

Programmatic (no config files)

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]');
}

From config files

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.

Configuration

@abbenay/core uses the same YAML config files as the full Abbenay daemon.

Config file locations

  • User level: ~/.config/abbenay/config.yaml
  • Workspace level: <workspace>/.config/abbenay/config.yaml

Config format

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 in chat() calls

Programmatic config access

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);

API Reference

CoreState

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
});

Builder Methods (in-memory, no disk writes)

MethodReturnsDescription
addProvider(id, options)Promise<void>Add a provider programmatically
removeProvider(id)booleanRemove an in-memory provider
addModel(providerId, modelName, config?)voidAdd a model to an existing provider
removeModel(providerId, modelName)booleanRemove a model from a provider
hasProvider(id)booleanCheck if a provider exists

Query & Chat Methods

MethodReturnsDescription
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

Builder API (addProvider / addModel)

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:

FieldTypeDescription
enginestringRequired. Engine type ("openai", "anthropic", etc.)
apiKeystringAPI key value — stored in SecretStore automatically
apiKeyEnvVarstringEnvironment variable name (alternative to apiKey)
baseUrlstringCustom base URL (overrides engine default)
modelsRecord<string, ModelConfig>Models to enable

In-memory providers merge over disk config. If both define the same provider ID, the in-memory version wins.

SecretStore

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>;
}

Chat streaming

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 finished

Tool calling

Pass 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 handles the tool execution loop automatically (up to maxSteps: 10).

Supported Engines

EngineIDRequires KeySDK PackageNotes
OpenAIopenaiYes@ai-sdk/openaiGPT-4o, o1, etc.
AnthropicanthropicYes@ai-sdk/anthropicClaude 4, 3.5, etc.
Google GeminigeminiYes@ai-sdk/googleGemini Pro, Flash
MistralmistralYes@ai-sdk/mistralMistral Large, etc.
xAI (Grok)xaiYes@ai-sdk/xaiGrok models
DeepSeekdeepseekYes@ai-sdk/deepseekDeepSeek Coder
GroqgroqYes@ai-sdk/groqFast inference
CoherecohereYes@ai-sdk/cohereCommand models
Amazon BedrockbedrockNo*@ai-sdk/amazon-bedrockUses AWS credentials
Vertex Anthropicvertex-anthropicNo*@ai-sdk/google-vertexClaude on Vertex AI; supports Bearer-token proxies
FireworksfireworksYes@ai-sdk/fireworksOptimized inference
Together AItogetheraiYes@ai-sdk/togetheraiOpen models
PerplexityperplexityYes@ai-sdk/perplexitySearch-augmented
Azure OpenAIazureYes@ai-sdk/openai-compatibleCustom base URL required
OpenRouteropenrouterYes@ai-sdk/openai-compatible100+ model aggregator
OllamaollamaNo@ai-sdk/openai-compatibleLocal models
LM StudiolmstudioNo@ai-sdk/openai-compatibleLocal models
CerebrascerebrasYes@ai-sdk/openai-compatibleFast inference
Meta (Llama)metaYes@ai-sdk/openai-compatibleLlama API
Red Hat AIredhatNo@ai-sdk/openai-compatibleInference Server or OpenShift AI MaaS
MockmockNo(built-in)Testing only

Policies

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.

Built-in policies

PolicyPurpose
preciseLow temperature (0.15), concise factual responses
balancedGeneral-purpose (temp 0.5, 4096 tokens)
creativeHigh temperature (0.9), longer output
coderLow temperature, complete runnable code
json_strictJSON-only output with retry on invalid JSON
long_context_chatConcise follow-ups in long conversations

Custom policies

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: 30000

Assigning to a model

providers:
  my-openai:
    engine: openai
    models:
      gpt-4o:
        policy: coder
        temperature: 0.1   # Overrides the policy's temperature

Programmatic access

import { resolvePolicy, listAllPolicies, flattenPolicy } from '@abbenay/core';

const policy = resolvePolicy('coder');
const all = listAllPolicies();            // Built-in + custom
const flat = flattenPolicy(policy!);      // { params, systemPromptSnippet, ... }

ToolRegistry

The ToolRegistry collects tools from multiple sources, namespaces them, and builds executors compatible with CoreState.chat().

Registering tools

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: ['...'] }),
  },
]);

Using tools with chat

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);
}

Tool policy

Control tool visibility and approval via ToolPolicyConfig:

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
});

Relationship to @abbenay/daemon

@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/.