Skip to content

Adding an AI Provider Adapter ​

Postmill's AI layer supports 30 providers through a pluggable adapter system. Each adapter lives in its own workspace package under libraries/providers/<id>/ and is registered into the ProviderKernel at backend boot.

Verified against v1.3.0


Decision: bespoke vs OpenAI-compatible ​

Before writing code, decide which pattern applies:

PatternWhen to useExample
Bespoke adapterProvider has its own @ai-sdk/* package (e.g. @ai-sdk/anthropic, @ai-sdk/google)OpenAI, Anthropic, Google, xAI, Mistral
OpenAICompatibleAdapterProvider exposes an OpenAI-compatible API endpointSiliconFlow, DeepInfra, MiniMax, Qwen

For OpenAI-compatible providers, instantiate OpenAICompatibleAdapter from @postmill-ai/provider-kernel with a base URL and capabilities. No bespoke class is needed.

For bespoke providers, create a class implementing AiCapability from @postmill-ai/provider-kernel.


Step 1: Create the provider package ​

Add a workspace package at libraries/providers/<id>/. At minimum it needs:

libraries/providers/<id>/
├── package.json
├── src/
│   ├── index.ts
│   └── v1/
│       ├── index.ts
│       ├── ai.adapter.ts
│       └── metadata.ts

package.json example:

json
{
  "name": "@postmill-ai/provider-yourprovider",
  "version": "1.0.0",
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "dependencies": {
    "@postmill-ai/provider-kernel": "workspace:*"
  }
}

Add the package to pnpm-workspace.yaml if it is not already covered by the existing glob.


Step 2: Implement the adapter ​

Create libraries/providers/<id>/src/v1/ai.adapter.ts.

Bespoke adapter ​

typescript
import { createYourProvider } from '@ai-sdk/yourprovider';
import { ChatYourProvider } from '@langchain/yourprovider';
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
import type {
  LanguageModelV2,
  ImageModelV2,
  EmbeddingModelV2,
  SpeechModelV2,
} from '@ai-sdk/provider-v5';
import {
  type AiCapability as AIProviderAdapter,
  type AiCredentialField as CredentialField,
  type AiModelInfo as ModelInfo,
  type AiCapabilities as AICapabilities,
  type AiModelOptions as AIModelOptions,
  type ProviderModule,
  type SafeFetchPort,
  fetchOpenAIStyleModels,
  mergeLiveModels,
} from '@postmill-ai/provider-kernel';
import { metadata as providerMetadata } from './metadata';

const CAPABILITIES: AICapabilities = {
  text: true,
  image: false,
  vision: false,
  embeddings: false,
  speech: false,
  tools: true,
};

// Static fallback catalog + curated metadata (labels, vision/reasoning flags)
// for the models you know about. Live listing merges with this.
const STATIC_MODELS: ModelInfo[] = [
  {
    id: 'model-v1',
    label: 'Model V1',
    kind: 'text',
    capabilities: CAPABILITIES,
  },
];

const CREDENTIAL_FIELDS: CredentialField[] = [
  {
    key: 'apiKey',
    label: 'API Key',
    type: 'password',
    required: true,
    placeholder: 'Enter your API key',
  },
];

export class YourProviderAdapter implements AIProviderAdapter {
  readonly identifier = 'yourprovider';
  readonly name = 'Your Provider';
  readonly type = 'direct' as const;
  readonly credentialFields = CREDENTIAL_FIELDS;
  readonly capabilities = CAPABILITIES;

  private _safeFetch?: SafeFetchPort;

  setSafeFetch(fetch: SafeFetchPort): void {
    this._safeFetch = fetch;
  }

  async listModels(creds: Record<string, string>): Promise<ModelInfo[]> {
    // Live-first: enumerate the provider's real catalog over the SSRF-safe fetch
    // and merge it with the static fallback (curated labels/capabilities win on
    // known ids). On ANY failure the static list is returned unchanged. See
    // ai-architecture.md → "Model catalogs (live-first)".
    const live = await fetchOpenAIStyleModels(
      this._safeFetch,
      creds.baseURL || 'https://api.yourprovider.com/v1',
      creds.apiKey,
    );
    return mergeLiveModels(live, STATIC_MODELS, this.capabilities);
  }

  async validateCredentials(
    creds: Record<string, string>,
  ): Promise<{ ok: boolean; error?: string }> {
    if (!creds.apiKey) {
      return { ok: false, error: 'API key is required' };
    }
    if (!this._safeFetch) {
      return { ok: false, error: 'cannot validate' };
    }
    try {
      const response = await this._safeFetch('https://api.yourprovider.com/v1/models', {
        headers: { Authorization: `Bearer ${creds.apiKey}` },
      });
      if (response.ok) return { ok: true };
      if (response.status === 401 || response.status === 403) {
        return { ok: false, error: 'Invalid API key' };
      }
      return { ok: false, error: `Unexpected response: ${response.status}` };
    } catch (err: any) {
      return { ok: false, error: err.message };
    }
  }

  createLanguageModel(
    creds: Record<string, string>,
    modelId: string,
    _opts?: AIModelOptions,
  ): LanguageModelV2 {
    const provider = createYourProvider({ apiKey: creds.apiKey });
    return provider.languageModel(modelId);
  }

  createLangchainModel(
    creds: Record<string, string>,
    modelId: string,
    opts?: AIModelOptions,
  ): BaseChatModel {
    return new ChatYourProvider({
      apiKey: creds.apiKey,
      model: modelId,
      temperature: opts?.temperature,
      topP: opts?.topP,
      maxTokens: opts?.maxTokens,
    });
  }
}

const adapter = new YourProviderAdapter();

export const yourproviderAiModule: ProviderModule<any, any> = {
  metadata: providerMetadata,
  manifest: {
    domain: 'ai',
    providerId: adapter.identifier,
    version: 'v1',
    displayName: adapter.name,
    status: 'active',
    credentialFields: adapter.credentialFields,
    capabilities: adapter.capabilities,
  },
  create: (ctx) => {
    adapter.setSafeFetch(ctx.fetch);
    return adapter as any;
  },
  validateCredentials: async (ctx) => {
    adapter.setSafeFetch(ctx.fetch);
    return adapter.validateCredentials(ctx.credentials);
  },
};

OpenAI-compatible adapter ​

typescript
import { OpenAICompatibleAdapter, type ProviderModule } from '@postmill-ai/provider-kernel';
import { metadata as providerMetadata } from './metadata';

const adapter = new OpenAICompatibleAdapter(
  'yourprovider',
  'Your Provider',
  'https://api.yourprovider.com/v1',
  { vision: true },
  [
    {
      id: 'model-v1',
      label: 'Model V1',
      kind: 'text',
      capabilities: {
        text: true,
        image: false,
        vision: true,
        embeddings: false,
        speech: false,
        tools: true,
      },
    },
  ],
  'hub',
);

export const yourproviderAiModule: ProviderModule<any, any> = {
  metadata: providerMetadata,
  manifest: {
    domain: 'ai',
    providerId: adapter.identifier,
    version: 'v1',
    displayName: adapter.name,
    status: 'active',
    credentialFields: (adapter as any).credentialFields || [],
    capabilities: (adapter as any).capabilities,
  },
  create: (ctx) => {
    adapter.setSafeFetch(ctx.fetch);
    return adapter as any;
  },
  validateCredentials: async (ctx) => {
    adapter.setSafeFetch(ctx.fetch);
    return adapter.validateCredentials(ctx.credentials);
  },
};

The OpenAICompatibleAdapter automatically:

  • Uses @ai-sdk/openai under the hood
  • Tries to fetch the model list from {baseURL}/models if credentials are provided
  • Falls back to the provided default models if the /models endpoint is unreachable
  • Supports createImageModel, createEmbeddingModel, and createSpeechModel when the underlying API does

Step 3: Export the module ​

libraries/providers/<id>/src/v1/index.ts:

typescript
export { yourproviderAiModule } from './ai.adapter';

libraries/providers/<id>/src/index.ts:

typescript
import { yourproviderAiModule } from './v1';

export default [yourproviderAiModule];

Step 4: Register in the backend manifest ​

Wire the workspace package up in three places:

  1. apps/backend/package.json — add "@postmill-ai/provider-yourprovider": "workspace:*" (alphabetical), then run pnpm install.
  2. tsconfig.base.json — add the two path aliases (alphabetical):
json
"@postmill-ai/provider-yourprovider": ["libraries/providers/yourprovider/src"],
"@postmill-ai/provider-yourprovider/*": ["libraries/providers/yourprovider/src/*"],
  1. Add the import and array entry to apps/backend/src/providers.generated.ts:
typescript
import yourproviderModules from '@postmill-ai/provider-yourprovider';

export const providerModules = [
  // ... existing providers
  ...yourproviderModules,
];

ProvidersBootstrap registers every module into the kernel at boot. If the ai feature flag is enabled (DEV_DISABLE_AI is not set), your provider appears in the catalog and can be selected in Settings → AI.

Base URL visibility: the Settings → AI form hides the baseURL credential field for every provider except endpoint-bringing ones allowlisted in BASE_URL_PROVIDERS (apps/frontend/src/components/settings/shared/kit/descriptors/ai.descriptor.ts) — currently only openai-compatible. If your provider has no canonical endpoint of its own, add its id there (and pass { requireBaseURL: true } to OpenAICompatibleAdapter). Saved baseURL values must still be public HTTPS URLs — OrgAiSettingsService._assertBaseURLSafe rejects private/loopback/non-HTTPS hosts.


Step 5: Credential encryption ​

Credentials are encrypted at rest via EncryptionService (AES-256-GCM, v2: prefix). The adapter receives decrypted credentials at call time through the kernel's ProviderRuntimeContext — it never stores or logs them. No additional work is needed on the adapter's part.


Step 6: Tests ​

Write an adapter spec in the provider package, e.g. libraries/providers/<id>/src/v1/ai.adapter.spec.ts:

  • validateCredentials() with valid and invalid credentials
  • listModels() returns the expected shape
  • createLanguageModel() / createLangchainModel() return valid model objects
  • Optional methods return correct types or undefined

Mock the underlying AI SDK or the injected SafeFetchPort rather than making real API calls.


Current Adapter Inventory ​

Bespoke adapters (16) ​

openai, anthropic, google, bedrock, vertex, azure, groq, fireworks, togetherai, deepseek, mistral, cohere, perplexity, xai, gateway, openrouter

OpenAI-compatible adapters (14) ​

siliconflow, deepinfra, minimax, qwen, meta-llama, gmihub, bitdeer, lightning, vultr, kimi, zai, apertus, nvidia, openai-compatible

Total: 30 providers.

The AI-native social media management platform — postmill.ai