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 inapps/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)
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.
params shape; chunks include cumulative content and may report token usage on the final chunk.
Observability
Instrumentation records StatsD-style metrics and feature loggerai 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.- Ensure
tooling-ai-anthropic,tooling-ai-openai, and/ortooling-ai-googleare listed inapps/backend/package.json(they ship with the boilerplate). - Add a
tools.aiblock toapps/backend/src/configuration/development.ts(andproduction.tswhen you go live). Example:
- Set API keys in
.env.development(see provider pages for variable names). - Implement your feature in
apps/backend/src/core/and calltools.ai.completewith an allowlistedmodelor omitmodelfor the primary provider default. - Run
make test module=shared path=apps/shared/src/ai/__tests__andmake 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 inapps/tools/ai/*/src/client.ts. Instrumentation inapps/backend/src/tools/ai/instrumentation.tsis the single owner.
❌ Never assume an arbitrary model string will work. Unknown ids throwAiValidationError; user-facing model pickers should usegetAvailableModelsor 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?
- Anthropic — Claude setup and env vars
- OpenAI — GPT setup and token estimate behavior
- Google Gemini — Gemini setup and system prompts
- Configuration — env files and pluggable tools table
- Tooling system — how loaders and workspace packages fit together
