Browse documentation

Specifications

RankFlow AI — AI Services Documentation

│ AI Service Layer │

docs/specs/ai-services.md
On this page

Version: 1.0.0
Service Path: src/server/services/ai/
Providers: Anthropic (Claude), OpenAI (GPT-4o)
Router: Multi-LLM with task-based model selection


1. Architecture Overview#

┌─────────────────────────────────────────────────────────────┐
│                    AI Service Layer                          │
│                                                              │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐   │
│  │   Router    │───→│  Anthropic  │    │   OpenAI    │   │
│  │  (main.ts)  │    │  (claude)   │    │  (gpt-4o)   │   │
│  └─────────────┘    └─────────────┘    └─────────────┘   │
│        │                                                     │
│        ↓                                                     │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐   │
│  │  Prompts    │    │  Templates  │    │   Audit     │   │
│  │  (prompts.ts)│   │  (DB)       │    │  (metrics)  │   │
│  └─────────────┘    └─────────────┘    └─────────────┘   │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Design Principles#

  1. Task-based routing: Each content type maps to an optimal model
  2. Cost optimization: Cheaper models for simple tasks, expensive for complex
  3. Automatic fallback: If primary model fails, try secondary
  4. Full audit trail: Every generation logged with cost, latency, tokens
  5. Template-driven: Prompts stored in DB, versioned, A/B testable

2. Multi-LLM Router#

Core Interface#

// src/server/services/ai/router.ts

interface GenerateOptions {
  task: string;                          // Task type key
  system?: string;                       // System prompt override
  prompt: string;                        // User prompt
  model?: "claude-sonnet" | "claude-haiku" | "gpt-4o" | "gpt-4o-mini";
  maxTokens?: number;                    // Default: 1024
  temperature?: number;                  // Default: 0.7
  jsonMode?: boolean;                    // Force JSON output
}

interface GenerateResult {
  text: string;                          // Generated content
  model: string;                         // Actual model used
  provider: string;                      // "anthropic" | "openai"
  tokensUsed: number;                   // Total tokens
  costUsd: number;                      // Calculated cost
  latencyMs: number;                    // Generation time
}

Router Logic#

1. Receive generate() call with task type
2. Look up model mapping (TASK_MODELS)
3. Load prompt template from DB (if exists)
4. Call provider SDK
5. Calculate cost from token usage
6. Log to audit system
7. Return result
8. On error → fallback to next model

3. Model Configuration#

Supported Models#

Model Key Provider Model ID Input Cost Output Cost Context
claude-sonnet Anthropic claude-sonnet-4-20250514 $3.00/M $15.00/M 200K
claude-haiku Anthropic claude-3-5-haiku-20241022 $0.25/M $1.25/M 200K
gpt-4o OpenAI gpt-4o $2.50/M $10.00/M 128K
gpt-4o-mini OpenAI gpt-4o-mini $0.15/M $0.60/M 128K

Cost Calculation#

const costUsd = 
  (inputTokens / 1_000_000) * config.inputCost +
  (outputTokens / 1_000_000) * config.outputCost;

Model Selection Criteria#

Criteria Preferred Model
Complex reasoning Claude Sonnet
Speed + cost Claude Haiku / GPT-4o Mini
JSON structured output GPT-4o
Creative writing Claude Sonnet
Simple classification GPT-4o Mini
Medical compliance Claude Sonnet

4. Task-to-Model Mapping#

Default Mappings#

Task Default Model Rationale
gbp_post claude-haiku Short, factual, cost-sensitive
landing_page_article claude-sonnet Long-form, SEO-optimized
faq claude-haiku Structured Q&A
citation_description gpt-4o-mini Simple, 30 variations needed
social_caption gpt-4o-mini Short, casual tone
review_reply claude-haiku Empathetic, quick
schema_markup claude-sonnet Structured JSON-LD
seo_audit claude-sonnet Complex analysis
meta_description gpt-4o-mini 160 chars, simple
content_evolution claude-sonnet Strategic updates
report_summary claude-sonnet Data synthesis
keyword_research gpt-4o Structured data

Override Rules#

// Client can override via API
const result = await ai.generate({
  task: "gbp_post",
  model: "claude-sonnet",  // Override default haiku
  prompt: "...",
});

// Admin can set practice-level defaults
await db.promptTemplate.update({
  where: { practiceId_taskType: { practiceId: "xxx", taskType: "gbp_post" } },
  data: { modelConfig: { model: "claude-sonnet" } },
});

5. Prompt Template System#

Database Schema#

model PromptTemplate {
  id              String   @id @default(cuid())
  practiceId      String?  // null = global default
  taskType        String   // gbp_post, landing_page, faq, etc.
  name            String
  version         Int      @default(1)
  isDefault       Boolean  @default(false)
  systemPrompt    String   @db.Text
  userPromptTemplate String @db.Text
  outputSchema    Json?    // Expected output structure
  modelConfig     Json     // { provider, model, temperature, maxTokens }
  variant         String   @default("A")
  performanceScore Decimal? @db.Decimal(5, 2)
  createdAt       DateTime @default(now())
  updatedAt       DateTime @updatedAt
}

Template Resolution Order#

1. Practice-specific template (isDefault=true, latest version)
2. Global default template (practiceId=null, isDefault=true)
3. Hardcoded fallback prompt

Variable Substitution#

// Template: "Write a GBP post for {{practiceName}} in {{city}}"
// Variables: { practiceName: "Dr. Smith Clinic", city: "Kochi" }
// Result: "Write a GBP post for Dr. Smith Clinic in Kochi"

let prompt = template.userPromptTemplate;
for (const [key, value] of Object.entries(variables)) {
  prompt = prompt.replace(new RegExp(`{{${key}}}`, "g"), String(value));
}

A/B Testing#

Variant Traffic Performance Score
A (control) 50% 4.2/5
B (test) 50% 4.5/5

Auto-promote winner when performance score > control + 0.3


6. Cost Tracking#

Per-Request Logging#

logger.info({
  event: "ai_generation",
  task: options.task,
  model: modelKey,
  provider: config.provider,
  tokensUsed: inputTokens + outputTokens,
  inputTokens,
  outputTokens,
  costUsd,
  latencyMs,
  practiceId: context.practiceId,
  userId: context.userId,
  requestId: context.requestId,
}, "AI generation completed");

Monthly Cost Aggregation#

Dimension Tracked
By practice Per-client AI spend
By task Which tasks cost most
By model Claude vs OpenAI split
By day Daily burn rate

Cost Alerts#

Threshold Action
> $50/day for practice Notify admin
> $500/month for practice Flag for review
> $1000/day system-wide Throttle non-essential tasks

7. Fallback Chain#

Fallback Rules#

Primary Model Fails
        ↓
┌─────────────────┐
│ claude-sonnet   │ → Try gpt-4o
│ claude-haiku    │ → Try gpt-4o-mini
│ gpt-4o          │ → Try claude-sonnet
│ gpt-4o-mini     │ → Try claude-haiku
└─────────────────┘
        ↓
All models fail
        ↓
  Return error
  Log incident
  Notify admin

Fallback Implementation#

try {
  return await generateWithModel(primaryModel);
} catch (error) {
  const fallback = getFallbackModel(primaryModel);
  logger.info({ from: primaryModel, to: fallback }, "Falling back");
  return generateWithModel(fallback);
}

Circuit Breaker#

Condition State Action
5 failures in 1 min OPEN Block for 30s
1 success HALF-OPEN Allow 1 request
3 successes CLOSED Normal flow

8. Provider Integrations#

Anthropic (Claude)#

// src/server/services/ai/providers/anthropic.ts

import { createAnthropic } from "@ai-sdk/anthropic";
import { generateText } from "ai";

const anthropic = createAnthropic({ 
  apiKey: process.env.ANTHROPIC_API_KEY 
});

export async function generateWithClaude(
  model: string,
  options: GenerateOptions
): Promise<GenerateResult> {
  const result = await generateText({
    model: anthropic(model),
    system: options.system,
    prompt: options.prompt,
    maxTokens: options.maxTokens || 1024,
    temperature: options.temperature || 0.7,
  });
  
  return {
    text: result.text,
    model,
    provider: "anthropic",
    tokensUsed: result.usage.promptTokens + result.usage.completionTokens,
    costUsd: calculateCost("anthropic", model, result.usage),
    latencyMs: Date.now() - startTime,
  };
}

OpenAI (GPT-4o)#

// src/server/services/ai/providers/openai.ts

import { createOpenAI } from "@ai-sdk/openai";
import { generateText } from "ai";

const openai = createOpenAI({ 
  apiKey: process.env.OPENAI_API_KEY 
});

export async function generateWithOpenAI(
  model: string,
  options: GenerateOptions
): Promise<GenerateResult> {
  const result = await generateText({
    model: openai(model),
    system: options.system,
    prompt: options.prompt,
    maxTokens: options.maxTokens || 1024,
    temperature: options.temperature || 0.7,
  });
  
  return {
    text: result.text,
    model,
    provider: "openai",
    tokensUsed: result.usage.promptTokens + result.usage.completionTokens,
    costUsd: calculateCost("openai", model, result.usage),
    latencyMs: Date.now() - startTime,
  };
}

9. Content Types#

GBP Post Generation#

Input: Practice name, location, services, tone
Output: 150-300 character post with CTA
Constraints: Medical advertising compliance, no guarantees

Landing Page Article#

Input: Practice info, target keywords, word count
Output: 500-1000 word SEO article
Constraints: Original content, keyword density < 2%, readable

Review Reply#

Input: Review text, rating, practice name
Output: Empathetic, professional reply
Constraints: HIPAA-aware (no PHI), apologetic for negative

Citation Description#

Input: Practice NAP, services, directory name
Output: 100-200 word unique description
Constraints: Unique per directory, no duplicates

Social Caption#

Input: Platform, topic, tone
Output: Platform-optimized caption
Constraints: Character limits, hashtag rules

Schema Markup#

Input: Practice type, services, locations
Output: Valid JSON-LD
Constraints: Schema.org compliant, Google-validated

FAQ Generation#

Input: Practice specialty, common questions
Output: 5-10 Q&A pairs
Constraints: Accurate medical info, disclaimer if needed


10. Usage Examples#

Basic Generation#

import { ai } from "@/server/services/ai/router";

const result = await ai.generate({
  task: "gbp_post",
  prompt: `Generate a Google Business Profile post for "Dr. Smith Dental Clinic" 
           in Kochi. Focus on teeth whitening services. Include a call to action.`,
});

console.log(result.text);        // Generated post
console.log(result.costUsd);     // $0.002
console.log(result.latencyMs);   // 1200

Template-Based Generation#

const result = await ai.generateWithTemplate(
  practiceId,
  "landing_page_article",
  {
    practiceName: "Dr. Smith Dental Clinic",
    city: "Kochi",
    services: "Teeth Whitening, Root Canal, Braces",
    targetKeyword: "best dentist in Kochi",
  }
);

With Override#

const result = await ai.generate({
  task: "seo_audit",
  model: "claude-sonnet",  // Override default
  prompt: "Analyze this website for SEO issues...",
  maxTokens: 4096,          // Longer output
  temperature: 0.3,         // More deterministic
});

Streaming (for UI)#

import { streamText } from "ai";

const stream = await streamText({
  model: anthropic("claude-haiku"),
  prompt: "Write a social media post...",
});

// Return stream to client for real-time display
return stream.toDataStreamResponse();

API Endpoints#

content.generate (tRPC)#

// Client calls this to trigger AI generation
const mutation = api.content.generate.useMutation();

mutation.mutate({
  taskType: "GBP_POST",
  locationId: "loc_123",
  variables: {
    topic: "New dental implant technology",
    tone: "professional",
  },
});

Response#

{
  "id": "content_abc123",
  "type": "GBP_POST",
  "status": "PENDING_REVIEW",
  "title": null,
  "content": "Discover the latest in dental implant technology at Dr. Smith Dental Clinic...",
  "aiGenerated": true,
  "aiProvider": "anthropic",
  "aiModel": "claude-3-5-haiku-20241022",
  "aiTokensUsed": 245,
  "costUsd": 0.00031,
  "generationTimeMs": 890,
  "humanEdited": false,
  "createdAt": "2025-01-15T10:30:00Z"
}

End of AI Services Documentation