Skip to main content
Version: 57.2.0

@terreno/ai

AI service layer for Terreno backends: provider-agnostic chat via the Vercel AI SDK, request logging, GPT history, projects, file uploads, MCP tools, and optional Langfuse integration.

Table of Contents

Install

bun add @terreno/ai @terreno/api mongoose

Peer dependencies: @terreno/api, mongoose (^8.0.0). Consuming apps also install a Vercel AI SDK provider (e.g. @ai-sdk/google) for their chosen model.

Commands

From the @terreno/ai package directory:

bun run compile # Compile TypeScript
bun run dev # Watch mode (tsc -w)
bun run test # Run tests (with bunSetup preload)
bun run lint # Lint code
bun run lint:fix # Fix lint issues

Architecture

src/
index.ts # Public exports
aiApp.ts # AiApp TerrenoPlugin
langfuseApp.ts # LangfuseApp TerrenoPlugin
langfuseClient.ts # Langfuse SDK client lifecycle
langfuseCache.ts # Prompt/trace caching
langfusePrompts.ts # Prompt compile/fetch helpers
langfuseTracing.ts # OpenTelemetry tracing setup
langfuseVercelAi.ts # Bridge Langfuse prompts to Vercel AI SDK
models/
aiRequest.ts # AI request logging model
gptHistory.ts # Conversation history model
fileAttachment.ts # Uploaded file metadata
project.ts # GPT project + memories
routes/
gpt.ts # Streaming chat, remix, tools, ratings
gptHistories.ts # History CRUD
aiRequestsExplorer.ts # Admin request explorer
files.ts # File upload/signed URL/delete
projects.ts # Project CRUD + memories
mcp.ts # MCP server status and tools
service/
aiService.ts # Provider-agnostic AI service
fileStorage.ts # GCS upload helper
getMCPTools.ts # modelRouter MCP tools as Vercel AI SDK tools
mcpService.ts # MCP client connections
parseAiJson.ts # LLM JSON normalization/parsing
prompts.ts # System prompt constants
gemini.ts # Gemini Developer API model listing
vertex.ts # Vertex AI provider helpers
webSearchTool.ts # WebSearchProvider interface
types/ # Shared TypeScript types

Key exports

  • Plugins: AiApp, LangfuseApp
  • Service: AIService, TemperaturePresets, FileStorageService, MCPService, getMCPTools
  • Models: AIRequest, GptHistory, FileAttachment, Project
  • Routes: addGptRoutes, addGptHistoryRoutes, addAiRequestsExplorerRoutes, addFileRoutes, addProjectRoutes, addMcpRoutes
  • Structured output: parseAiJson, normalizeLlmJsonTextForStructuredOutput, re-exported Output, jsonSchema, JSONValue, FlexibleSchema from ai
  • Langfuse: initLangfuseClient, getLangfuseClient, shutdownLangfuseClient, compilePrompt, createPrompt, getPrompt, createTelemetryConfig, preparePromptForAI, initTracing, shutdownTracing, LangfuseCache, cache helpers
  • Gemini / Vertex: listGeminiApiModels, normalizeGeminiModelId, GEMINI_API_BASE_URL, createVertexProvider, listEnabledVertexModels, verifyVertexModelsEnabled, assertVertexModelsEnabled, isVertexModelAllowed, normalizeVertexModelId, DEFAULT_VERTEX_LOCATION
  • Prompts: CONTENT_SUMMARY_PROMPT, DEFAULT_GPT_MEMORY, JSON_VALUE_SYSTEM_PROMPT, REMIX_PROMPT, TITLE_GENERATION_PROMPT, TRANSLATION_PROMPT
  • Web search: WebSearchProvider, WebSearchResult types

AIService

Provider-agnostic wrapper around a Vercel AI SDK LanguageModel. The consuming app supplies the model instance.

import {AIService} from "@terreno/ai";
import {google} from "@ai-sdk/google";

const aiService = new AIService({
model: google("gemini-2.5-flash"),
defaultTemperature: 1.0,
});

Constructor options

OptionDescription
modelVercel AI SDK LanguageModel instance (required)
defaultTemperatureDefault temperature for text/stream calls (default: TemperaturePresets.DEFAULT)

Properties

PropertyDescription
modelConfigured LanguageModel
defaultTemperatureDefault temperature
modelIdResolved model identifier string

Methods

MethodDescription
generateText(options)Non-streaming text generation; logs as requestType: "general"
generateJsonValue(options)Any JSON value via Output.json(); logs as "json_value"
generateJsonObject(options)Typed object from schema/Zod via Output.object(); logs as "json_object"
generateJsonArray(options)Typed array via Output.array(); logs as "json_array"
generateTextStream(options)Async generator of text chunks; logs full response after stream completes
generateRemix(options)Reword text using REMIX_PROMPT at TemperaturePresets.BALANCED
generateSummary(options)Summarize text using CONTENT_SUMMARY_PROMPT at TemperaturePresets.LOW
translateText(options)Translate text using TRANSLATION_PROMPT at TemperaturePresets.LOW
buildMessages(prompts)Convert GptHistoryPrompt[] to Vercel AI SDK ModelMessage[] (skips tool-call/result entries)
generateChatStream(options)Stream multi-turn chat with optional tools; logs prompt as joined message text

All generation methods log to AIRequest via private logRequest(). Logging failures never throw.

Structured JSON output

generateJsonValue, generateJsonObject, and generateJsonArray:

  • Default to TemperaturePresets.DETERMINISTIC when temperature is omitted.
  • Use JSON_VALUE_SYSTEM_PROMPT when systemPrompt is omitted.
  • Run model text through normalizeLlmJsonTextForStructuredOutput before Vercel Output.* parsing (strips fences, preamble, balanced JSON slice, trailing commas, smart-quote repair).
  • On failure: logger.error records prompt, system prompt, raw model text, and error details; AIRequest stores response (raw text or sentinel), error, and metadata (system, finishReason, errorStack, rawModelTextCaptured).

Standalone helpers:

import {parseAiJson, normalizeLlmJsonTextForStructuredOutput} from "@terreno/ai";

const result = parseAiJson<MyType>(rawLlmText);
if (result.success) {
console.info(result.data);
}

Re-exported from ai for schema building: Output, jsonSchema, types JSONValue, FlexibleSchema.

TemperaturePresets

import {TemperaturePresets} from "@terreno/ai";

TemperaturePresets.DETERMINISTIC // 0
TemperaturePresets.LOW // 0.3
TemperaturePresets.BALANCED // 0.7
TemperaturePresets.DEFAULT // 1.0
TemperaturePresets.HIGH // 1.5
TemperaturePresets.MAXIMUM // 2.0

Models

AIRequest

Logs all AI calls for monitoring and admin explorer.

FieldTypeDescription
aiModelstringModel identifier (field name avoids Mongoose model conflict)
promptstringInput prompt
requestTypestringe.g. general, remix, summarization, translation, json_value, json_object, json_array
responsestring?Response text
responseTimenumber?Milliseconds
tokensUsednumber?Total tokens
userIdObjectId?Requesting user
errorstring?Error message
metadataMixed?Extra data (e.g. structured-output debug)
parentRequestIdObjectId?Parent in multi-agent workflow
subRequestIdsObjectId[]?Child request refs
totalResponseTimenumber?Combined sub-request time
totalTokensUsednumber?Combined sub-request tokens

Statics: AIRequest.logRequest(params), AIRequest.logMultiAgentRequest(params)

Plugins: createdUpdatedPlugin, isDeletedPlugin, findOneOrNone, findExactlyOne

GptHistory

Conversation history with multi-modal prompts.

FieldTypeDescription
userIdObjectIdOwner (required)
titlestring?Auto-generated on first /gpt/prompt response when empty
projectIdObjectId?Optional project association
promptsarrayMessages: text, type (user | assistant | system | tool-call | tool-result), optional content parts, model, rating, tool fields

Virtual: ownerId aliases userId for Permissions.IsOwner.

FileAttachment

Metadata for files stored in GCS.

FieldTypeDescription
userIdObjectIdUploader
filenamestringOriginal filename
gcsKeystringUnique GCS object key
mimeTypestringMIME type
sizenumberBytes
urlstringPublic GCS URL

Virtual: ownerId aliases userId.

Project

GPT project with persistent context and memories.

FieldTypeDescription
userIdObjectIdOwner
namestringProject name
systemContextstringPrepended to every chat in this project
memoriesarray{text, category?, source: "user" | "auto"} entries

Virtual: ownerId aliases userId.

Route registrars

addGptRoutes(router, options)

EndpointMethodAuthDescription
/gpt/promptPOSTIsAuthenticatedSSE streaming chat; body: prompt, optional historyId, systemPrompt, attachments, model, projectId
/gpt/remixPOSTIsAuthenticatedNon-streaming text remix; body: {text}
/gpt/histories/:id/ratingPATCHIsAuthenticatedRate a prompt; body: {promptIndex, rating: "up" | "down" | null}
/gpt/toolsGETIsAuthenticatedList builtin + MCP tools

AI resolution order: x-ai-api-key header + createModelFncreateServerModelFn(modelId) → configured aiService → demo SSE response when demoMode and none available.

addGptHistoryRoutes(router, options?)

CRUD at /gpt/histories via modelRouter:

OperationPermission
Create, ListIsAuthenticated
Read, Update, DeleteIsOwner

Query filtered by userId; sort -updated; query fields userId, projectId.

addProjectRoutes(router, options?)

EndpointMethodAuthDescription
/gpt/projects/:id/memoriesPOSTIsAuthenticated (owner)Add memory; body: {text, category?}
/gpt/projects/:id/memories/:memoryIdDELETEIsAuthenticated (owner)Remove memory
/gpt/projectsCRUDCreate/List: IsAuthenticated; Read/Update/Delete: IsOwnerStandard modelRouter

addFileRoutes(router, options)

Requires fileStorageService and gcsBucket (registered by AiApp when both are set).

EndpointMethodAuthDescription
/files/uploadPOSTIsAuthenticatedMultipart upload (file field); allowed MIME: images, PDF, plain text, CSV, JSON
/files/*gcsKeyGETNoneReturns signed read URL (1 hour)
/files/*gcsKeyDELETEIsAuthenticated (owner)Soft-delete attachment and remove from GCS

addMcpRoutes(router, options)

Requires mcpService (registered by AiApp when set).

EndpointMethodAuthDescription
/mcp/serversGETIsAuthenticated + adminServer connection status
/mcp/toolsGETIsAuthenticatedAvailable MCP tools
/mcp/servers/:name/reconnectPOSTIsAuthenticated + adminReconnect one server

addAiRequestsExplorerRoutes(router, options?)

EndpointMethodAuthDescription
/aiRequestsExplorerGETIsAuthenticated + user.adminPaginated AI request log; filters: requestType, model, startDate, endDate

AiApp plugin

AiApp registers all AI routes in one TerrenoPlugin:

import {AiApp, AIService, FileStorageService, MCPService} from "@terreno/ai";
import {google} from "@ai-sdk/google";

const aiService = new AIService({model: google("gemini-2.5-flash")});

new AiApp({
aiService,
fileStorageService: new FileStorageService({bucketName: "my-bucket"}),
gcsBucket: "my-bucket",
mcpService: new MCPService([{name: "tools", transport: {type: "sse", url: "..."}}]),
tools: myToolDefinitions,
demoMode: false,
createModelFn: (apiKey, modelId) => google(modelId ?? "gemini-2.5-flash", {apiKey}),
openApiOptions: options,
}).register(app);
OptionDescription
aiServicePre-configured server-wide AI service
createModelFnBuild model from per-request x-ai-api-key
createServerModelFnServer-side model factory (e.g. Vertex ADC) without per-request key
demoModeReturn canned responses when no AI service resolves
fileStorageService + gcsBucketEnable file upload routes
mcpServiceEnable MCP routes and tool discovery in chat
toolsStatic Vercel AI SDK tool definitions for chat
toolChoice"auto" | "none" | "required" (default "auto" when tools present)
maxStepsMax tool-calling steps (default 5)
titleModelIdCheaper model for conversation title generation
openApiOptionsPassed to route OpenAPI builders

LangfuseApp plugin

Optional Langfuse admin UI and tracing:

import {LangfuseApp} from "@terreno/ai";

new LangfuseApp({
publicKey: process.env.LANGFUSE_PUBLIC_KEY!,
secretKey: process.env.LANGFUSE_SECRET_KEY!,
baseUrl: process.env.LANGFUSE_BASE_URL,
adminPath: "/admin/langfuse",
enableTracing: true,
enableAdminUI: true,
evaluation: {enabled: true, scoringFunctions: [...]},
}).register(app);
OptionDefaultDescription
publicKey, secretKeyLangfuse API keys (required)
baseUrlLangfuse host
adminPath"/admin/langfuse"Admin route prefix
organizationmaintainer defaultLangfuse organization slug — set your own
project"terreno"Langfuse project slug — set your own
enableTracingtrueOpenTelemetry via @langfuse/otel
enableAdminUItruePrompt, trace, playground, evaluation routes
evaluation.enabledRegister evaluation scoring routes
cachePrompt/trace TTL overrides

Calls shutdownLangfuseClient() and shutdownTracing() on SIGTERM.

Langfuse integration

Low-level exports (also used by addGptRoutes when langfuseSystemPromptName is set):

  • Client: initLangfuseClient, getLangfuseClient, isLangfuseInitialized, shutdownLangfuseClient
  • Prompts: getPrompt, createPrompt, compilePrompt, invalidatePromptCache, preparePromptForAI
  • Tracing: initTracing, shutdownTracing, createTelemetryConfig
  • Cache: LangfuseCache, getCached, setCached, invalidateCache

Subpath imports for tree-shaking: @terreno/ai/langfuseClient, @terreno/ai/langfuseApp.

FileStorageService

Google Cloud Storage helper for uploads referenced by addFileRoutes.

const storage = new FileStorageService({
bucketName: "my-bucket",
storageOptions: {}, // optional @google-cloud/storage options
});

await storage.upload({buffer, filename, mimeType, userId});
await storage.getSignedUrl(gcsKey); // 1-hour v4 signed URL
await storage.delete(gcsKey); // GCS delete + soft-delete FileAttachment

getMCPTools

Wraps registered modelRouter MCP tools as Vercel AI SDK Tool objects for in-process streamText / generateText. HTTP MCP clients still use POST /mcp from @terreno/api; this helper is the chat-route path.

import {getMCPTools} from "@terreno/ai";

const tools = getMCPTools(req.user);

MCPService

Manages SSE MCP client connections for tool calling.

const mcp = new MCPService([
{name: "my-server", transport: {type: "sse", url: "https://...", headers: {...}}},
]);
await mcp.connect();
const tools = await mcp.getTools();
const status = mcp.getServerStatus();
await mcp.reconnectServer("my-server");
await mcp.disconnect();

Gemini and Vertex helpers

Gemini Developer API (API-key based):

import {listGeminiApiModels, normalizeGeminiModelId, GEMINI_API_BASE_URL} from "@terreno/ai";

const models = await listGeminiApiModels({apiKey: "..."});

Vertex AI / Gemini Enterprise:

import {
createVertexProvider,
listEnabledVertexModels,
assertVertexModelsEnabled,
DEFAULT_VERTEX_LOCATION,
} from "@terreno/ai";

const vertex = await createVertexProvider({project: "my-gcp-project"});
const model = vertex.languageModel("gemini-2.5-flash");

Env fallbacks: GOOGLE_VERTEX_PROJECT, GOOGLE_VERTEX_LOCATION (default us-central1).

Web search types

WebSearchProvider and WebSearchResult define a pluggable search interface for custom Vercel AI SDK tools. The package does not ship a default provider — implement search(query) and wire it into a tool() passed to AiApp tools.

Integration example

import {TerrenoApp} from "@terreno/api";
import {AiApp, AIService, LangfuseApp} from "@terreno/ai";
import {google} from "@ai-sdk/google";

const aiService = new AIService({model: google("gemini-2.5-flash")});

new TerrenoApp({userModel: User})
.register(new AiApp({aiService, openApiOptions: {}}))
.register(
new LangfuseApp({
publicKey: process.env.LANGFUSE_PUBLIC_KEY!,
secretKey: process.env.LANGFUSE_SECRET_KEY!,
})
)
.start();

Legacy setupServer pattern: call addGptHistoryRoutes, addGptRoutes, etc. inside addRoutes.

Environment variables

VariableUsed byDescription
GOOGLE_VERTEX_PROJECTcreateVertexProviderGCP project for Vertex models
GOOGLE_VERTEX_LOCATIONcreateVertexProviderVertex region (default us-central1)
LANGFUSE_PUBLIC_KEYLangfuseAppLangfuse public key
LANGFUSE_SECRET_KEYLangfuseAppLangfuse secret key
LANGFUSE_BASE_URLLangfuse clientLangfuse host URL

GCS credentials use standard Google Cloud Application Default Credentials for FileStorageService.

Conventions

  • Use aiModel on AIRequest, not model (Mongoose reserved name).
  • GptHistory, FileAttachment, and Project use userId with ownerId virtual for Permissions.IsOwner.
  • Gpt history list uses queryFilter: (user) => ({userId: user?.id}), not OwnerQueryFilter.
  • Express user in routes: (req.user as {_id?: ObjectId}) casting pattern.
  • Throw APIError with appropriate status; check conditions early.
  • Uses Model.findOneOrNone / findExactlyOne — never raw findOne.

Testing

  • Framework: bun test with preload ./src/tests/bunSetup.ts
  • HTTP: supertest against real routes
  • DB: memory Mongo via @terreno/test (TERRENO_TEST_USE_MEMORY_MONGO or TERRENO_TEST_MONGODB_URI)
  • Mock AI model: implement doGenerate and doStream on a fake LanguageModel
const createMockModel = () => ({
doGenerate: mock(async () => ({
finishReason: "stop" as const,
rawCall: {rawPrompt: "", rawSettings: {}},
text: "response text",
usage: {completionTokens: 10, promptTokens: 5},
})),
doStream: mock(async () => ({
rawCall: {rawPrompt: "", rawSettings: {}},
stream: new ReadableStream({
start(controller) {
controller.enqueue({type: "text-delta" as const, textDelta: "chunk "});
controller.enqueue({
type: "finish" as const,
finishReason: "stop" as const,
usage: {completionTokens: 10, promptTokens: 5},
});
controller.close();
},
}),
})),
modelId: "mock-model",
provider: "mock-provider",
specificationVersion: "v1" as const,
});

Never mock @terreno/api or Mongoose models — test against real functionality.