Skip to content

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:

  1. Native Function Calling (tools): Used by app.agent() and Gemini Live Voice to project bounded action spaces into provider-native tool declarations.
  2. 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/anthropic
bash
npm install @invariant/google @invariant/openai @invariant/anthropic
bash
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)

ConstantModel IDRecommended 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)

ConstantModel IDRecommended 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)

ConstantModel IDRecommended 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:

  1. Full IDE IntelliSense: Typing model names gives immediate autocomplete for all known models.
  2. 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

  1. Zero Vendor Lock-In: Swap from OpenAI to an in-house model without modifying any workflow DSL nodes or agent contracts.
  2. Uniform Validation Boundary: Workflow reasoning results pass through the declared runtime schema before REASON_COMPLETED can be committed. Provider failures become REASON_FAILED; the Beta does not schedule automatic retries.
  3. Community & Ecosystem: Teams can publish custom adapters to npm (e.g., invariant-adapter-groq, invariant-adapter-bedrock) as standalone modular packages.

Invariant Durable Execution Engine.