Skip to main content
This page explains how Refract wires Anthropic, OpenAI, and Google Gemini through a single tools.ai client with shared validation, timeouts, and metrics.
📖 See also: AI Development covers Cursor rules and agent workflows — not LLM API tooling.

How it works

Refract treats AI like other pluggable tools: the contract lives in apps/shared, vendor SDKs live in apps/tools/ai/<provider>/, and the backend loader returns one instrumented client on tools.ai. Unlike mailer or queue, you can enable more than one provider at once. Each provider block in config lists an allowlist of model ids. The loader routes calls by params.model; when the model is omitted, the primary provider is the first one loaded (anthropic → openai → google).

Configuration shape

tools.ai is optional. If you omit it from development.ts / production.ts, buildTools leaves tools.ai undefined. At least one provider block is required when tools.ai is present. Zod enforces defaultModel ∈ models when set.

Request policy (tools.ai.request)

Retries on complete are not idempotent. A timeout after the provider already finished can produce a second billed generation on retry. Tune totalTimeoutMs and maxAttempts for your cost tolerance.

Model routing

getAvailableModels(tools) projects the union of configured allowlists — use it to populate admin UI or validate user-facing model pickers.

Calling tools.ai

Every method takes (params, tools). Pass the same tools object the app already builds at startup so instrumentation can read tools.logger and tools.metrics.
Streaming uses the same params shape; chunks include cumulative content and may report token usage on the final chunk.

Observability

Instrumentation records StatsD-style metrics and feature logger ai entries: Operations: complete, stream, count_tokens, list_models. OpenAI countTokens logs tokenEstimate: true because the adapter uses a character heuristic, not the provider tokenizer API. Provider setup guides: Anthropic, OpenAI, Google Gemini.

Extending this system

Here’s the usual path from “enable AI” to a production call site.
  1. Ensure tooling-ai-anthropic, tooling-ai-openai, and/or tooling-ai-google are listed in apps/backend/package.json (they ship with the boilerplate).
  2. Add a tools.ai block to apps/backend/src/configuration/development.ts (and production.ts when you go live). Example:
  1. Set API keys in .env.development (see provider pages for variable names).
  2. Implement your feature in apps/backend/src/core/ and call tools.ai.complete with an allowlisted model or omit model for the primary provider default.
  3. Run make test module=shared path=apps/shared/src/ai/__tests__ and make test module=backend path=apps/backend/src/tools/ai/__tests__/instrumentation.spec.ts.

What not to do

❌ Never import vendor LLM SDKs in resolvers, utilities outside the tooling packages, or the portal. Use tools.ai only.
❌ Never duplicate retry or metrics in apps/tools/ai/*/src/client.ts. Instrumentation in apps/backend/src/tools/ai/instrumentation.ts is the single owner.
❌ Never assume an arbitrary model string will work. Unknown ids throw AiValidationError; user-facing model pickers should use getAvailableModels or your own allowlist derived from config.
❌ Never confuse this page with AI Development. That doc is for Cursor agents, not tools.ai.

File conventions

New AI-related code belongs in these trees: Provider packages export buildAnthropicClient, buildOpenaiClient, or buildGoogleClient from src/index.ts. The loader dynamic-imports them and never re-exports SDK types to the rest of the backend.

What’s next?