Model Adapters
Invariant provides first-class, lightweight model adapters for Google Gemini, OpenAI, and Anthropic Claude.
Adapters bridge Invariant workflows (.reason()) and conversational agents (app.agent()) to model providers with two distinct execution modes:
- Native Function Calling (
tools): Used byapp.agent()and Gemini Live Voice to project bounded action spaces into provider-native tool declarations. - Structured JSON Output (
schema): Used by workflow.reason()nodes to perform deterministic classification and extraction conforming to strict schemas.
Installation & Provider Types
Invariant model adapters execute lightweight non-streaming HTTP calls directly and re-export selected request/response types from the official provider SDKs (@google/genai, openai, @anthropic-ai/sdk).
bash
pnpm add @invariant/google @invariant/openai @invariant/anthropicbash
npm install @invariant/google @invariant/openai @invariant/anthropicbash
yarn add @invariant/google @invariant/openai @invariant/anthropic**Core Design Philosophy: Zero Type Erasure**
"Infrastructure boundaries should preserve domain types, not erase them." You get compile-time IntelliSense and type-safety directly from the underlying vendor types without adding heavy runtime dependencies to your bundle.
1. Google Gemini (@invariant/google)
Connects Invariant directly to Google Gemini models using secure header authentication (x-goog-api-key) and non-streaming REST generation.
Official Models Constant Map
ts
import { ChatGoogle, GEMINI_MODELS, GEMINI_MODEL_CATALOG } from '@invariant/google';
export const modelAdapter = new ChatGoogle({
apiKey: process.env.GEMINI_API_KEY,
defaultModel: GEMINI_MODELS.GEMINI_2_5_FLASH, // Default: 'gemini-2.5-flash'
});Supported Official Models (GEMINI_MODELS)
| Constant | Model ID | Recommended Use Case |
|---|---|---|
GEMINI_3_7_FLASH | 'gemini-3.7-flash' | Flagship agentic execution, complex multi-step reasoning, low token overhead |
GEMINI_3_6_FLASH | 'gemini-3.6-flash' | Multi-step agentic workflows and fast tool execution |
GEMINI_3_5_FLASH | 'gemini-3.5-flash' | Near-Pro intelligence at Flash-tier speed & cost |
GEMINI_3_5_FLASH_LITE | 'gemini-3.5-flash-lite' | Ultra-fast lightweight reasoning |
GEMINI_3_1_FLASH_LITE | 'gemini-3.1-flash-lite' | High-throughput, cost-sensitive classification |
GEMINI_2_5_PRO | 'gemini-2.5-pro' | Deep thinking, complex coding, and 1M token context |
GEMINI_2_5_FLASH | 'gemini-2.5-flash' | Default for agents: High speed with controllable thinking budgets |
GEMINI_2_5_FLASH_LITE | 'gemini-2.5-flash-lite' | Massive scale throughput |
GEMINI_2_5_FLASH_LIVE | 'gemini-2.5-flash-native-audio-preview-12-2025' | Real-time bidirectional voice & audio WebSockets |
2. OpenAI (@invariant/openai)
Connects Invariant to OpenAI models (GPT-4o, o1, o3-mini) with native tools and JSON schema mode.
ts
import { ChatOpenAI, OPENAI_MODELS } from '@invariant/openai';
export const openaiAdapter = new ChatOpenAI({
apiKey: process.env.OPENAI_API_KEY,
defaultModel: OPENAI_MODELS.GPT_4O, // Default: 'gpt-4o'
});Supported Official Models (OPENAI_MODELS)
| Constant | Model ID | Recommended Use Case |
|---|---|---|
GPT_4O | 'gpt-4o' | Flagship multimodal reasoning and tool calling |
GPT_4O_MINI | 'gpt-4o-mini' | Fast, cost-efficient agent turns |
O1 | 'o1' | Deep mathematical and logical reasoning |
O3_MINI | 'o3-mini' | High-speed structured reasoning |
GPT_4_TURBO | 'gpt-4-turbo' | High-throughput production tasks |
3. Anthropic (@invariant/anthropic)
Connects Invariant to Claude models (Claude 3.7 / 3.5 Sonnet) with native tool use and structured extraction.
ts
import { ChatAnthropic, ANTHROPIC_MODELS } from '@invariant/anthropic';
export const anthropicAdapter = new ChatAnthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
defaultModel: ANTHROPIC_MODELS.CLAUDE_3_5_SONNET,
});Supported Official Models (ANTHROPIC_MODELS)
| Constant | Model ID | Recommended Use Case |
|---|---|---|
CLAUDE_3_7_SONNET | 'claude-3-7-sonnet-20250219' | Hybrid reasoning (fast responses + extended thinking) |
CLAUDE_3_5_SONNET | 'claude-3-5-sonnet-20241022' | Standard for coding and workflow reasoning |
CLAUDE_3_5_HAIKU | 'claude-3-5-haiku-20241022' | Low-latency classification |
CLAUDE_3_OPUS | 'claude-3-opus-20240229' | Complex analysis tasks |
4. Extensibility: Zero-Lock-In Custom Models
Invariant uses the Autocomplete-Preserving Literal Union Pattern (KnownModelName | (string & {})).
This means:
- Full IDE IntelliSense: Typing model names gives immediate autocomplete for all known models.
- Zero Block / Future-Proof: You can pass newly released models or custom fine-tuned endpoints without waiting for SDK updates or using
as any.
ts
// Custom fine-tuned Vertex AI endpoint or newly released model:
const customAdapter = new ChatGoogle({
defaultModel: 'gemini-4.0-flash-preview', // 👈 Supported immediately without type error
});
// The adapter owns its configured model/endpoint identity:
const endpointAdapter = new ChatGoogle({
defaultModel: 'projects/my-org/locations/us-central1/endpoints/custom-salon-v2',
});
const turn = await endpointAdapter.generateReasoning({
instruction: 'Classify salon booking intent',
context: { userMessage: 'I need a haircut tomorrow' },
});5. Registering Adapters in Invariant
Pass your model adapters when initializing invariant():
ts
import { invariant } from '@invariant/sdk';
import { ChatGoogle, GEMINI_MODELS } from '@invariant/google';
import { ChatOpenAI, OPENAI_MODELS } from '@invariant/openai';
export const app = invariant({
models: {
default: new ChatGoogle({ defaultModel: GEMINI_MODELS.GEMINI_2_5_FLASH }),
reasoning: new ChatOpenAI({ defaultModel: OPENAI_MODELS.O3_MINI }),
},
session: { ... },
});
// Use in agents:
export const bookingAgent = app.agent('booking-agent', {
instructions: 'Interpret booking intent and use only the registered workflow.',
model: 'default',
workflows: [bookingWorkflow],
projection: ({ session }) => ({ customerTier: session.context.customerTier }),
});
// Use in workflow .reason() steps:
export const intentWorkflow = app.workflow('intent-classifier')
.reason('classify', {
model: 'reasoning',
instruction: 'Extract customer intent from message',
schema: IntentSchema,
});6. Live vs. Explicit Sandbox Mode
Provider adapters default to mode: "live". Missing credentials, network failures, and non-success provider responses throw; they are never converted into a fake successful model result.
Use sandbox mode only when a deterministic offline response is intentional:
ts
const offlineGoogle = new ChatGoogle({
defaultModel: GEMINI_MODELS.GEMINI_2_5_FLASH,
mode: "sandbox",
});For application tests, a dedicated RuntimeModelAdapter fixture that returns the exact schema under test is usually clearer than a provider sandbox.
7. Building Custom Model Adapters (Ollama, DeepSeek, vLLM, Groq)
Invariant is open by contract. The core runtime does not enforce any specific LLM provider. Any developer or enterprise can connect any LLM (local models running in Ollama, an internal vLLM cluster in Kubernetes, DeepSeek, Groq, Mistral, or a fine-tuned endpoint) by implementing the ModelAdapter interface from @invariant/core.
The ModelAdapter Contract
A custom adapter requires only a single method: generateReasoning(params):
ts
import type { ModelAdapter, ModelReasonParams, ModelReasonResponse } from '@invariant/core';
export interface ModelAdapter<TModel extends string = string> {
readonly provider: string;
readonly defaultModel: TModel;
generateReasoning(params: ModelReasonParams): Promise<ModelReasonResponse>;
}Complete Implementation Example: ChatDeepSeek / Custom OpenAI-Compatible Endpoint
Many providers (DeepSeek, Groq, Ollama, Together AI, vLLM, LocalAI) expose an OpenAI-compatible REST endpoint. Here is a minimal adapter boundary; add provider-specific timeouts, retries outside the SDK retry contract, telemetry, and response hardening before production use:
ts
import type { ModelAdapter, ModelReasonParams, ModelReasonResponse } from '@invariant/core';
// 1. Define model names (preserves IDE autocomplete while allowing any custom model)
export type DeepSeekModel = 'deepseek-reasoner' | 'deepseek-chat' | (string & {});
export class ChatDeepSeek implements ModelAdapter<DeepSeekModel> {
public readonly provider = 'deepseek';
public readonly defaultModel: DeepSeekModel;
private readonly apiKey: string;
private readonly baseUrl: string;
constructor(options?: { apiKey?: string; defaultModel?: DeepSeekModel; baseUrl?: string }) {
this.apiKey = options?.apiKey || process.env.DEEPSEEK_API_KEY || '';
this.defaultModel = options?.defaultModel ?? 'deepseek-reasoner';
this.baseUrl = options?.baseUrl ?? 'https://api.deepseek.com/v1';
}
async generateReasoning(params: ModelReasonParams): Promise<ModelReasonResponse> {
const modelToUse = this.defaultModel;
const response = await fetch(`${this.baseUrl}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.apiKey}`,
},
body: JSON.stringify({
model: modelToUse,
messages: [
...(params.instruction ? [{ role: 'system', content: params.instruction }] : []),
{ role: 'user', content: JSON.stringify(params.context, null, 2) },
],
// Support structured JSON output mode when requested by .reason()
...(params.schema ? { response_format: { type: 'json_object' } } : {}),
// Support tool calling when requested by app.agent()
...(params.tools && params.tools.length > 0
? {
tools: params.tools.map(t => ({
type: 'function',
function: { name: t.name, description: t.description, parameters: t.parameters },
})),
}
: {}),
}),
});
if (!response.ok) {
throw new Error(`DeepSeek API error (${response.status}): ${await response.text()}`);
}
const data = (await response.json()) as any;
const choice = data.choices?.[0]?.message;
const toolCall = choice?.tool_calls?.[0]?.function;
return {
text: choice?.content || undefined,
result: params.schema && choice?.content ? JSON.parse(choice.content) : undefined,
toolCall: toolCall
? {
name: toolCall.name,
args: toolCall.arguments ? JSON.parse(toolCall.arguments) : {},
}
: undefined,
usage: {
promptTokens: data.usage?.prompt_tokens ?? 0,
completionTokens: data.usage?.completion_tokens ?? 0,
},
};
}
}Local / On-Premise LLM Example: ChatOllama
For air-gapped or privacy-restricted environments (HIPAA / GDPR):
ts
import type { ModelAdapter, ModelReasonParams, ModelReasonResponse } from '@invariant/core';
export class ChatOllama implements ModelAdapter<string> {
public readonly provider = 'ollama';
public readonly defaultModel: string;
private readonly baseUrl: string;
constructor(options?: { defaultModel?: string; baseUrl?: string }) {
this.defaultModel = options?.defaultModel ?? 'llama3.3:70b';
this.baseUrl = options?.baseUrl ?? 'http://localhost:11434';
}
async generateReasoning(params: ModelReasonParams): Promise<ModelReasonResponse> {
const response = await fetch(`${this.baseUrl}/api/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: this.defaultModel,
system: params.instruction,
prompt: `Context:\n${JSON.stringify(params.context, null, 2)}`,
format: params.schema ? 'json' : undefined,
stream: false,
}),
});
const data = (await response.json()) as any;
return {
text: data.response,
result: params.schema && data.response ? JSON.parse(data.response) : undefined,
};
}
}Strategic Benefits of Invariant's Adapter Architecture
- Zero Vendor Lock-In: Swap from OpenAI to an in-house model without modifying any workflow DSL nodes or agent contracts.
- Uniform Validation Boundary: Workflow reasoning results pass through the declared runtime schema before
REASON_COMPLETEDcan be committed. Provider failures becomeREASON_FAILED; the Beta does not schedule automatic retries. - Community & Ecosystem: Teams can publish custom adapters to npm (e.g.,
invariant-adapter-groq,invariant-adapter-bedrock) as standalone modular packages.
8. Related Adapters & Roadmap
- Realtime Audio Adapter (@invariant/live-gemini) — Realtime WebSocket voice and audio integration.
- MCP Server Adapter (Roadmap) — Model Context Protocol tool bridge integration.