Skip to content

Building Adapters ​

Sails AI uses an adapter pattern to support any AI provider. Each adapter is a separate npm package that implements the Adapter interface. This guide shows you how to build one.

The Adapter interface ​

Adapters use duck typing; no inheritance or dummy chat implementation is required:

js
class MyAdapter {
  async initialize(config) {
    /* ... */
  }
  async chat({ messages, model }) {
    /* ... */
  }
  async *stream({ messages, model }) {
    /* ... */
  }
  async teardown() {
    /* ... */
  }
}

module.exports = MyAdapter
MethodRequiredDescription
initialize(config)YesCalled once during sails.lift(). Set up connections, validate credentials.
chat({ messages, model })For chat providersSend a chat completion request. Return { role, content, model }.
stream({ messages, model })NoAsync generator that yields { text, done } chunks.
teardown()NoClean up resources on sails.lower().

Speech adapters implement initialize(config), at least one submission method (transcribe or synthesize), and getJob(receipt, options). Providers expose boolean capabilities derived from implemented methods. See asynchronous speech for receipt and error semantics.

Scaffold a new adapter ​

Create a new package:

my-adapter/
├── index.js
└── package.json
json
// package.json
{
  "name": "@sails-ai/my-provider",
  "version": "0.0.1",
  "main": "index.js",
  "peerDependencies": {
    "sails-ai": ">=0.0.3"
  }
}

TIP

Declare a compatible sails-ai peer dependency for the hook contract. Core validates supported methods rather than class identity.

Implement the adapter ​

Here's a minimal adapter skeleton:

js
class MyAdapter {
  /**
   * Called once during sails.lift().
   * Use this to validate config, test connectivity, or cache credentials.
   */
  async initialize(config) {
    this.apiKey = config.apiKey
    this.baseUrl = config.baseUrl || 'https://api.my-provider.com'
    this.defaultModel = config.model || 'default-model'

    // Optional: test connectivity
    try {
      await fetch(`${this.baseUrl}/health`)
    } catch {
      console.warn(`@sails-ai/my-provider: Could not reach ${this.baseUrl}`)
    }
  }

  /**
   * Send a chat completion and return the full response.
   */
  async chat({ messages, model }) {
    const res = await fetch(`${this.baseUrl}/chat`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${this.apiKey}`
      },
      body: JSON.stringify({
        model: model || this.defaultModel,
        messages
      })
    })

    if (!res.ok) {
      const error = new Error(`Request failed: ${res.status}`)
      error.code = 'E_PROVIDER_ERROR'
      throw error
    }

    const data = await res.json()
    return {
      role: 'assistant',
      content: data.choices[0].message.content,
      model: model || this.defaultModel
    }
  }

  /**
   * Stream a chat completion token by token.
   */
  async *stream({ messages, model }) {
    const res = await fetch(`${this.baseUrl}/chat`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${this.apiKey}`
      },
      body: JSON.stringify({
        model: model || this.defaultModel,
        messages,
        stream: true
      })
    })

    const reader = res.body.getReader()
    const decoder = new TextDecoder()

    while (true) {
      const { done, value } = await reader.read()
      if (done) break

      const text = decoder.decode(value, { stream: true })
      yield { text, done: false }
    }

    yield { text: '', done: true, model: model || this.defaultModel }
  }
}

module.exports = MyAdapter

Error codes ​

Adapters should throw errors with a code property for programmatic handling:

CodeWhen
E_PROVIDER_UNAVAILABLECan't connect to the provider (network, server down)
E_MODEL_NOT_FOUNDRequested model doesn't exist on the provider
E_PROVIDER_ERRORProvider returned an error (bad request, rate limit, etc.)
E_AUTH_FAILEDInvalid credentials
js
const error = new Error('Could not connect to provider')
error.code = 'E_PROVIDER_UNAVAILABLE'
throw error

Using your adapter ​

Install it and add it to config/ai.js:

js
// config/ai.js
module.exports.ai = {
  provider: 'my-provider',
  providers: {
    'my-provider': {
      adapter: '@sails-ai/my-provider',
      apiKey: process.env.MY_PROVIDER_API_KEY,
      baseUrl: 'https://api.my-provider.com'
    }
  }
}

The config object is passed directly to your adapter's initialize() method, so you can add any provider-specific options you need.

Official adapters ​

AdapterProviderPackage
Local (Ollama)Ollama@sails-ai/local
OpenAITogether AI, Groq, Fireworks, OpenRouter, Mistral, OpenAI@sails-ai/openai

Check the GitHub repository for the latest list.

The YarnGPT adapter (@sails-ai/[email protected]) implements the asynchronous speech contract; it does not implement chat or streaming.

All open source projects are released under the MIT License.