Specifications
RankFlow AI — AI Services Documentation
│ AI Service Layer │
docs/specs/ai-services.mdOn this page
- 1. Architecture Overview
- Design Principles
- 2. Multi-LLM Router
- Core Interface
- Router Logic
- 3. Model Configuration
- Supported Models
- Cost Calculation
- Model Selection Criteria
- 4. Task-to-Model Mapping
- Default Mappings
- Override Rules
- 5. Prompt Template System
- Database Schema
- Template Resolution Order
- Variable Substitution
- A/B Testing
- 6. Cost Tracking
- Per-Request Logging
- Monthly Cost Aggregation
- Cost Alerts
- 7. Fallback Chain
- Fallback Rules
- Fallback Implementation
- Circuit Breaker
- 8. Provider Integrations
- Anthropic (Claude)
- OpenAI (GPT-4o)
- 9. Content Types
- GBP Post Generation
- Landing Page Article
- Review Reply
- Citation Description
- Social Caption
- Schema Markup
- FAQ Generation
- 10. Usage Examples
- Basic Generation
- Template-Based Generation
- With Override
- Streaming (for UI)
- API Endpoints
- content.generate (tRPC)
- Response
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#
- Task-based routing: Each content type maps to an optimal model
- Cost optimization: Cheaper models for simple tasks, expensive for complex
- Automatic fallback: If primary model fails, try secondary
- Full audit trail: Every generation logged with cost, latency, tokens
- 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