AI Integration
CruzJS ships two AI layers:
@cruzjs/coreAIService — adapter-native AI (Workers AI on Cloudflare, Bedrock on AWS, etc.) injected automatically based on deployment target.@cruzjs/ai— provider-agnostic AI with explicit providers (OpenAI, Anthropic, Cloudflare Gateway, OpenRouter), streaming, MCP tool-calling, org-scoped keys, and React hooks.
Most apps want @cruzjs/ai. It doesn’t assume a deployment target.
Install the package (already in the monorepo):
# @cruzjs/ai is in packages/ai — no npm install needed in the monorepoRegister the AIContainerModule in your app:
import { AIContainerModule } from '@cruzjs/ai';import { createCruzApp } from '@cruzjs/core';
export default createCruzApp({ modules: [/* your modules */], containerModules: [AIContainerModule],});Register your chosen provider:
// In a startup hook or module initializerimport { OpenAIProvider, AnthropicProvider } from '@cruzjs/ai';
const registry = container.get(AI_PROVIDER_REGISTRY);registry.register(new OpenAIProvider({ apiKey: process.env.OPENAI_API_KEY! }));registry.register(new AnthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY! }));Set the default provider via env var:
CRUZJS_AI_PROVIDER=openaiProviders
Section titled “Providers”All providers implement IAIProvider and use raw fetch() — no external SDKs required.
| Provider | Class | API |
|---|---|---|
| OpenAI | OpenAIProvider | api.openai.com |
| Anthropic | AnthropicProvider | api.anthropic.com |
| Cloudflare AI Gateway | CloudflareGatewayProvider | Your CF Gateway URL |
| OpenRouter | OpenRouterProvider | openrouter.ai |
import { OpenAIProvider, AnthropicProvider, CloudflareGatewayProvider, OpenRouterProvider } from '@cruzjs/ai';
// OpenAInew OpenAIProvider({ apiKey: 'sk-...', defaultModel: 'gpt-4o' })
// Anthropicnew AnthropicProvider({ apiKey: 'sk-ant-...', defaultModel: 'claude-sonnet-4-6' })
// Cloudflare AI Gateway (OpenAI-compatible)new CloudflareGatewayProvider({ accountId: 'abc123', gatewayId: 'my-gateway', apiKey: 'cf-...', defaultModel: '@cf/meta/llama-3.1-8b-instruct',})
// OpenRouter (any model via one key)new OpenRouterProvider({ apiKey: 'sk-or-...', defaultModel: 'anthropic/claude-sonnet-4-6', siteUrl: 'https://myapp.com', siteName: 'My App',})import { AIProviderRegistry, AI_PROVIDER_REGISTRY } from '@cruzjs/ai';
@injectable()export class PostsService { constructor( @inject(AI_PROVIDER_REGISTRY) private aiRegistry: AIProviderRegistry, ) {}
async summarize(text: string): Promise<string> { const ai = this.aiRegistry.resolve(); // uses CRUZJS_AI_PROVIDER const response = await ai.chat([ { role: 'system', content: 'Summarize the following text concisely.' }, { role: 'user', content: text }, ]); return response.content; }}Streaming
Section titled “Streaming”Stream tokens as they arrive using stream(), which returns AsyncIterable<StreamChunk>:
const ai = this.aiRegistry.resolve();
for await (const chunk of ai.stream([ { role: 'user', content: 'Write a haiku about Cloudflare Workers.' },])) { if (!chunk.done) { process.stdout.write(chunk.chunk); }}React Hooks
Section titled “React Hooks”useStream
Section titled “useStream”Accumulates streaming chunks into a single text string:
import { useStream } from '@cruzjs/ai/hooks';
function StreamingResponse() { const { text, isStreaming, error, stream, reset } = useStream();
const handleClick = () => { const ai = /* get provider somehow */; stream(ai, [{ role: 'user', content: 'Tell me a joke.' }]); };
return ( <div> <button onClick={handleClick} disabled={isStreaming}> {isStreaming ? 'Streaming...' : 'Generate'} </button> {error && <p className="text-red-600">{error.message}</p>} <p>{text}</p> </div> );}useChat
Section titled “useChat”Multi-turn conversation with message history:
import { useChat } from '@cruzjs/ai/hooks';
function ChatInterface() { const { messages, send, isStreaming, reset } = useChat({ provider: myProvider });
return ( <div> {messages.map((m, i) => ( <div key={i} className={m.role === 'user' ? 'text-right' : 'text-left'}> {m.content} </div> ))} <button onClick={() => send('What is CruzJS?')} disabled={isStreaming}> Ask </button> </div> );}Tool Calling (MCP Bridge)
Section titled “Tool Calling (MCP Bridge)”McpBridge.runWithTools() runs the agentic tool-call loop: provider returns a tool call → your executor is called → result is appended → provider continues. Repeats up to maxRounds (default: 5).
import { McpBridge } from '@cruzjs/ai';
const result = await McpBridge.runWithTools(provider, [ { role: 'user', content: 'Search for posts about CruzJS and summarize them.' },], { tools: [ { name: 'search_posts', description: 'Search blog posts', parameters: { query: { type: 'string' } } }, ], executor: async (toolCall) => { if (toolCall.name === 'search_posts') { const posts = await searchPosts(toolCall.arguments.query as string); return JSON.stringify(posts); } return 'Tool not found'; }, maxRounds: 3,});
console.log(result.content); // Final answer after tool useconsole.log(result.rounds); // How many tool-call rounds happenedOrg-Scoped Keys
Section titled “Org-Scoped Keys”Let each org configure their own AI provider and API key:
import { OrgAIConfigService } from '@cruzjs/ai';
// Configure in admin settingsconst orgAIService = container.get(OrgAIConfigService);orgAIService.setOrgConfig('org_abc', { provider: 'anthropic', apiKey: 'sk-ant-...', defaultModel: 'claude-haiku-4-5-20251001', enabled: true,});
// Resolve org's provider in a tRPC procedureconst orgProvider = orgAIService.forOrg('org_abc', registry);if (!orgProvider) throw new TRPCError({ code: 'FORBIDDEN', message: 'AI not configured for this org' });
const response = await orgProvider.chat([{ role: 'user', content: input.prompt }]);Usage Tracking
Section titled “Usage Tracking”Track token consumption across all AI calls:
import { AIUsageTracker, AI_USAGE_TRACKER } from '@cruzjs/ai';
// After an AI call:const tracker = container.get(AI_USAGE_TRACKER);tracker.record({ orgId: ctx.org.orgId, provider: 'openai', model: 'gpt-4o', inputTokens: response.inputTokens ?? 0, outputTokens: response.outputTokens ?? 0, durationMs: Date.now() - startTime, timestamp: new Date(),});
// Query usage:const summary = tracker.getSummary('org_abc');// { totalInputTokens: 15000, totalOutputTokens: 3200, totalRequests: 42 }tRPC Integration
Section titled “tRPC Integration”Wire the AI tRPC router into your app:
import { aiTrpc } from '@cruzjs/ai';
const appRouter = router({ ...registerCruzCoreTrpcRouters(), ai: aiTrpc,});Available procedures:
ai.chat— mutation, takes{ messages, options? }, returnsAIResponseai.embed— mutation, takes{ texts }, returns{ embeddings: number[][] }ai.providers— query, returns list of registered provider names
Embeddings
Section titled “Embeddings”const ai = this.aiRegistry.resolve();const embeddings = await ai.embed(['first document', 'second document']);// embeddings: number[][] — one vector per textUse embeddings for semantic search, similarity scoring, or RAG pipelines.
Local Development
Section titled “Local Development”All providers use fetch() and work locally as long as you have API keys:
OPENAI_API_KEY=sk-...ANTHROPIC_API_KEY=sk-ant-...CRUZJS_AI_PROVIDER=openaiNo Wrangler or cloud-specific bindings needed for @cruzjs/ai.