Agent Skills Framework Extension
AI Integration Patterns Skill
When to Use This Skill
Use this skill when implementing ai integration patterns patterns in your codebase.
How to Use This Skill
- Review the patterns and examples below
- Apply the relevant patterns to your implementation
- Follow the best practices outlined in this skill
AI provider integration, model routing, fallback strategies, and prompt management.
Core Capabilities
- Multi-Provider Integration - OpenAI, Anthropic, Ollama, etc.
- Model Routing - Select optimal model per task
- Fallback Strategies - Graceful degradation
- Prompt Management - Templates and versioning
- Cost Optimization - Token tracking and budgets
Multi-Provider Client
// src/ai/providers/types.ts
export interface AIMessage {
role: 'system' | 'user' | 'assistant';
content: string;
}
export interface AICompletionOptions {
model?: string;
maxTokens?: number;
temperature?: number;
stream?: boolean;
stopSequences?: string[];
}
export interface AICompletionResult {
content: string;
model: string;
usage: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
};
finishReason: 'stop' | 'length' | 'content_filter';
latencyMs: number;
}
export interface AIProvider {
name: string;
complete(messages: AIMessage[], options?: AICompletionOptions): Promise<AICompletionResult>;
stream(messages: AIMessage[], options?: AICompletionOptions): AsyncIterable<string>;
isAvailable(): Promise<boolean>;
}
// src/ai/providers/anthropic.ts
import Anthropic from '@anthropic-ai/sdk';
export class AnthropicProvider implements AIProvider {
name = 'anthropic';
private client: Anthropic;
constructor(apiKey: string) {
this.client = new Anthropic({ apiKey });
}
async complete(messages: AIMessage[], options: AICompletionOptions = {}): Promise<AICompletionResult> {
const start = Date.now();
const systemMessage = messages.find(m => m.role === 'system');
const chatMessages = messages
.filter(m => m.role !== 'system')
.map(m => ({ role: m.role as 'user' | 'assistant', content: m.content }));
const response = await this.client.messages.create({
model: options.model || 'claude-sonnet-4-20250514',
max_tokens: options.maxTokens || 4096,
temperature: options.temperature ?? 0.7,
system: systemMessage?.content,
messages: chatMessages,
stop_sequences: options.stopSequences,
});
const textContent = response.content.find(c => c.type === 'text');
return {
content: textContent?.text || '',
model: response.model,
usage: {
promptTokens: response.usage.input_tokens,
completionTokens: response.usage.output_tokens,
totalTokens: response.usage.input_tokens + response.usage.output_tokens,
},
finishReason: response.stop_reason === 'end_turn' ? 'stop' : 'length',
latencyMs: Date.now() - start,
};
}
async *stream(messages: AIMessage[], options: AICompletionOptions = {}): AsyncIterable<string> {
const systemMessage = messages.find(m => m.role === 'system');
const chatMessages = messages
.filter(m => m.role !== 'system')
.map(m => ({ role: m.role as 'user' | 'assistant', content: m.content }));
const stream = await this.client.messages.stream({
model: options.model || 'claude-sonnet-4-20250514',
max_tokens: options.maxTokens || 4096,
temperature: options.temperature ?? 0.7,
system: systemMessage?.content,
messages: chatMessages,
});
for await (const event of stream) {
if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
yield event.delta.text;
}
}
}
async isAvailable(): Promise<boolean> {
try {
await this.client.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 10,
messages: [{ role: 'user', content: 'ping' }],
});
return true;
} catch {
return false;
}
}
}
// src/ai/providers/openai.ts
import OpenAI from 'openai';
export class OpenAIProvider implements AIProvider {
name = 'openai';
private client: OpenAI;
constructor(apiKey: string) {
this.client = new OpenAI({ apiKey });
}
async complete(messages: AIMessage[], options: AICompletionOptions = {}): Promise<AICompletionResult> {
const start = Date.now();
const response = await this.client.chat.completions.create({
model: options.model || 'gpt-4-turbo-preview',
max_tokens: options.maxTokens || 4096,
temperature: options.temperature ?? 0.7,
messages: messages.map(m => ({ role: m.role, content: m.content })),
stop: options.stopSequences,
});
const choice = response.choices[0];
return {
content: choice.message.content || '',
model: response.model,
usage: {
promptTokens: response.usage?.prompt_tokens || 0,
completionTokens: response.usage?.completion_tokens || 0,
totalTokens: response.usage?.total_tokens || 0,
},
finishReason: choice.finish_reason === 'stop' ? 'stop' : 'length',
latencyMs: Date.now() - start,
};
}
async *stream(messages: AIMessage[], options: AICompletionOptions = {}): AsyncIterable<string> {
const stream = await this.client.chat.completions.create({
model: options.model || 'gpt-4-turbo-preview',
max_tokens: options.maxTokens || 4096,
temperature: options.temperature ?? 0.7,
messages: messages.map(m => ({ role: m.role, content: m.content })),
stream: true,
});
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content;
if (content) yield content;
}
}
async isAvailable(): Promise<boolean> {
try {
await this.client.chat.completions.create({
model: 'gpt-3.5-turbo',
max_tokens: 10,
messages: [{ role: 'user', content: 'ping' }],
});
return true;
} catch {
return false;
}
}
}
Model Router
// src/ai/router/model-router.ts
import { AIProvider, AIMessage, AICompletionOptions, AICompletionResult } from '../providers/types';
interface ModelConfig {
provider: string;
model: string;
maxTokens: number;
costPer1kInput: number;
costPer1kOutput: number;
capabilities: string[];
priority: number;
}
interface RoutingRule {
name: string;
condition: (task: TaskContext) => boolean;
modelPreference: string[];
}
interface TaskContext {
taskType: 'code' | 'chat' | 'analysis' | 'creative' | 'summary';
complexity: 'low' | 'medium' | 'high';
maxBudget?: number;
requiredCapabilities?: string[];
preferredLatency?: 'fast' | 'normal' | 'slow';
}
export class ModelRouter {
private providers: Map<string, AIProvider> = new Map();
private models: Map<string, ModelConfig> = new Map();
private rules: RoutingRule[] = [];
private fallbackOrder: string[] = [];
constructor() {
this.initializeModels();
this.initializeRules();
}
private initializeModels(): void {
this.models.set('claude-opus', {
provider: 'anthropic',
model: 'claude-opus-4-20250514',
maxTokens: 200000,
costPer1kInput: 0.015,
costPer1kOutput: 0.075,
capabilities: ['code', 'analysis', 'creative', 'long-context', 'vision'],
priority: 1,
});
this.models.set('claude-sonnet', {
provider: 'anthropic',
model: 'claude-sonnet-4-20250514',
maxTokens: 200000,
costPer1kInput: 0.003,
costPer1kOutput: 0.015,
capabilities: ['code', 'analysis', 'creative', 'vision'],
priority: 2,
});
this.models.set('gpt-4-turbo', {
provider: 'openai',
model: 'gpt-4-turbo-preview',
maxTokens: 128000,
costPer1kInput: 0.01,
costPer1kOutput: 0.03,
capabilities: ['code', 'analysis', 'creative', 'vision'],
priority: 3,
});
this.models.set('gpt-4o', {
provider: 'openai',
model: 'gpt-4o',
maxTokens: 128000,
costPer1kInput: 0.005,
costPer1kOutput: 0.015,
capabilities: ['code', 'analysis', 'creative', 'vision', 'fast'],
priority: 2,
});
this.fallbackOrder = ['claude-sonnet', 'gpt-4o', 'claude-opus', 'gpt-4-turbo'];
}
private initializeRules(): void {
this.rules = [
{
name: 'high-complexity-code',
condition: (ctx) => ctx.taskType === 'code' && ctx.complexity === 'high',
modelPreference: ['claude-opus', 'gpt-4-turbo'],
},
{
name: 'fast-chat',
condition: (ctx) => ctx.taskType === 'chat' && ctx.preferredLatency === 'fast',
modelPreference: ['gpt-4o', 'claude-sonnet'],
},
{
name: 'budget-conscious',
condition: (ctx) => ctx.maxBudget !== undefined && ctx.maxBudget < 0.01,
modelPreference: ['gpt-4o', 'claude-sonnet'],
},
{
name: 'long-context',
condition: (ctx) => ctx.requiredCapabilities?.includes('long-context') ?? false,
modelPreference: ['claude-opus', 'claude-sonnet'],
},
];
}
registerProvider(name: string, provider: AIProvider): void {
this.providers.set(name, provider);
}
selectModel(context: TaskContext): ModelConfig {
// Find matching rule
for (const rule of this.rules) {
if (rule.condition(context)) {
for (const modelName of rule.modelPreference) {
const model = this.models.get(modelName);
if (model && this.providers.has(model.provider)) {
return model;
}
}
}
}
// Fallback to default order
for (const modelName of this.fallbackOrder) {
const model = this.models.get(modelName);
if (model && this.providers.has(model.provider)) {
return model;
}
}
throw new Error('No available model found');
}
async complete(
messages: AIMessage[],
context: TaskContext,
options?: AICompletionOptions
): Promise<AICompletionResult> {
const selectedModel = this.selectModel(context);
const provider = this.providers.get(selectedModel.provider);
if (!provider) {
throw new Error(`Provider not found: ${selectedModel.provider}`);
}
return provider.complete(messages, {
...options,
model: selectedModel.model,
});
}
async completeWithFallback(
messages: AIMessage[],
context: TaskContext,
options?: AICompletionOptions
): Promise<AICompletionResult> {
const errors: Error[] = [];
for (const modelName of this.fallbackOrder) {
const model = this.models.get(modelName);
if (!model) continue;
const provider = this.providers.get(model.provider);
if (!provider) continue;
try {
const isAvailable = await provider.isAvailable();
if (!isAvailable) continue;
return await provider.complete(messages, {
...options,
model: model.model,
});
} catch (error) {
errors.push(error as Error);
console.warn(`Model ${modelName} failed, trying next...`);
}
}
throw new AggregateError(errors, 'All models failed');
}
}
Prompt Template System
// src/ai/prompts/template-manager.ts
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
import Handlebars from 'handlebars';
interface PromptTemplate {
id: string;
version: string;
description: string;
template: string;
variables: string[];
examples?: Array<{
input: Record<string, unknown>;
output: string;
}>;
}
interface PromptVersion {
version: string;
template: PromptTemplate;
createdAt: Date;
metrics?: {
avgTokens: number;
successRate: number;
avgLatency: number;
};
}
export class PromptTemplateManager {
private templates: Map<string, PromptVersion[]> = new Map();
private compiled: Map<string, HandlebarsTemplateDelegate> = new Map();
constructor(private readonly templateDir: string) {
this.loadTemplates();
this.registerHelpers();
}
private loadTemplates(): void {
// Load templates from directory
const templateFiles = [
'code-review.yaml',
'code-generation.yaml',
'summarization.yaml',
'analysis.yaml',
];
for (const file of templateFiles) {
const path = join(this.templateDir, file);
if (existsSync(path)) {
const content = readFileSync(path, 'utf-8');
// Parse YAML and register template
// Implementation depends on yaml parser
}
}
}
private registerHelpers(): void {
Handlebars.registerHelper('json', (context) => JSON.stringify(context, null, 2));
Handlebars.registerHelper('truncate', (str, len) =>
str.length > len ? str.slice(0, len) + '...' : str
);
Handlebars.registerHelper('codeBlock', (code, lang) =>
`\`\`\`${lang || ''}\n${code}\n\`\`\``
);
}
register(template: PromptTemplate): void {
const versions = this.templates.get(template.id) || [];
versions.push({
version: template.version,
template,
createdAt: new Date(),
});
this.templates.set(template.id, versions);
// Compile template
const compiled = Handlebars.compile(template.template);
this.compiled.set(`${template.id}:${template.version}`, compiled);
}
render(
templateId: string,
variables: Record<string, unknown>,
version?: string
): string {
const versions = this.templates.get(templateId);
if (!versions || versions.length === 0) {
throw new Error(`Template not found: ${templateId}`);
}
// Get specific version or latest
const templateVersion = version
? versions.find(v => v.version === version)
: versions[versions.length - 1];
if (!templateVersion) {
throw new Error(`Template version not found: ${templateId}:${version}`);
}
const key = `${templateId}:${templateVersion.version}`;
const compiled = this.compiled.get(key);
if (!compiled) {
throw new Error(`Template not compiled: ${key}`);
}
// Validate required variables
for (const varName of templateVersion.template.variables) {
if (!(varName in variables)) {
throw new Error(`Missing required variable: ${varName}`);
}
}
return compiled(variables);
}
getLatestVersion(templateId: string): string | undefined {
const versions = this.templates.get(templateId);
return versions?.[versions.length - 1]?.version;
}
listTemplates(): Array<{ id: string; versions: string[] }> {
const result: Array<{ id: string; versions: string[] }> = [];
for (const [id, versions] of this.templates) {
result.push({
id,
versions: versions.map(v => v.version),
});
}
return result;
}
}
// Example template
const codeReviewTemplate: PromptTemplate = {
id: 'code-review',
version: '1.0.0',
description: 'Review code for issues and improvements',
template: `You are a senior software engineer reviewing code.
## Code to Review
{{codeBlock code language}}
## Review Focus
{{#each focus}}
- {{this}}
{{/each}}
## Instructions
1. Identify bugs and security issues
2. Suggest improvements for readability and performance
3. Check for best practices violations
4. Rate overall code quality (1-10)
Provide your review in the following format:
### Issues Found
[List critical issues]
### Suggestions
[List improvement suggestions]
### Quality Score
[Score]/10 - [Brief justification]`,
variables: ['code', 'language', 'focus'],
};
Cost Tracking
// src/ai/cost/tracker.ts
interface UsageRecord {
timestamp: Date;
model: string;
provider: string;
promptTokens: number;
completionTokens: number;
cost: number;
taskType: string;
userId?: string;
}
interface CostSummary {
totalCost: number;
byProvider: Record<string, number>;
byModel: Record<string, number>;
byTaskType: Record<string, number>;
tokenUsage: {
prompt: number;
completion: number;
total: number;
};
}
export class CostTracker {
private records: UsageRecord[] = [];
private modelCosts: Map<string, { input: number; output: number }> = new Map();
constructor() {
this.initializeCosts();
}
private initializeCosts(): void {
// Cost per 1K tokens
this.modelCosts.set('claude-opus-4-20250514', { input: 0.015, output: 0.075 });
this.modelCosts.set('claude-sonnet-4-20250514', { input: 0.003, output: 0.015 });
this.modelCosts.set('gpt-4-turbo-preview', { input: 0.01, output: 0.03 });
this.modelCosts.set('gpt-4o', { input: 0.005, output: 0.015 });
}
record(result: AICompletionResult, taskType: string, userId?: string): void {
const costs = this.modelCosts.get(result.model);
const cost = costs
? (result.usage.promptTokens / 1000) * costs.input +
(result.usage.completionTokens / 1000) * costs.output
: 0;
this.records.push({
timestamp: new Date(),
model: result.model,
provider: this.getProvider(result.model),
promptTokens: result.usage.promptTokens,
completionTokens: result.usage.completionTokens,
cost,
taskType,
userId,
});
}
private getProvider(model: string): string {
if (model.startsWith('claude')) return 'anthropic';
if (model.startsWith('gpt')) return 'openai';
return 'unknown';
}
getSummary(since?: Date): CostSummary {
const filtered = since
? this.records.filter(r => r.timestamp >= since)
: this.records;
const summary: CostSummary = {
totalCost: 0,
byProvider: {},
byModel: {},
byTaskType: {},
tokenUsage: { prompt: 0, completion: 0, total: 0 },
};
for (const record of filtered) {
summary.totalCost += record.cost;
summary.byProvider[record.provider] = (summary.byProvider[record.provider] || 0) + record.cost;
summary.byModel[record.model] = (summary.byModel[record.model] || 0) + record.cost;
summary.byTaskType[record.taskType] = (summary.byTaskType[record.taskType] || 0) + record.cost;
summary.tokenUsage.prompt += record.promptTokens;
summary.tokenUsage.completion += record.completionTokens;
summary.tokenUsage.total += record.promptTokens + record.completionTokens;
}
return summary;
}
getBudgetStatus(budget: number, period: 'day' | 'week' | 'month'): {
used: number;
remaining: number;
percentUsed: number;
} {
const now = new Date();
let since: Date;
switch (period) {
case 'day':
since = new Date(now.getFullYear(), now.getMonth(), now.getDate());
break;
case 'week':
since = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
break;
case 'month':
since = new Date(now.getFullYear(), now.getMonth(), 1);
break;
}
const summary = this.getSummary(since);
return {
used: summary.totalCost,
remaining: Math.max(0, budget - summary.totalCost),
percentUsed: (summary.totalCost / budget) * 100,
};
}
}
Usage Examples
Setup Multi-Provider Client
Apply ai-integration-patterns skill to configure AI client with Anthropic and OpenAI fallback
Implement Model Routing
Apply ai-integration-patterns skill to create intelligent model router based on task complexity
Track AI Costs
Apply ai-integration-patterns skill to implement cost tracking with budget alerts
Integration Points
- error-handling-resilience - Fallback and retry logic
- caching-strategies - Response caching for repeated queries
- monitoring-observability - Usage metrics and dashboards
Success Output
When successful, this skill MUST output:
✅ SKILL COMPLETE: ai-integration-patterns
Completed:
- [x] Multi-provider client interfaces implemented (Anthropic, OpenAI)
- [x] Model router configured with fallback strategies
- [x] Prompt template manager operational with versioning
- [x] Cost tracking system capturing all API calls
- [x] Provider availability checks functional
- [x] Token usage monitoring active
Outputs:
- src/ai/providers/types.ts (shared interfaces)
- src/ai/providers/anthropic.ts (Anthropic integration)
- src/ai/providers/openai.ts (OpenAI integration)
- src/ai/router/model-router.ts (intelligent routing)
- src/ai/prompts/template-manager.ts (prompt management)
- src/ai/cost/tracker.ts (cost tracking)
- Budget monitoring dashboard configured
Completion Checklist
Before marking this skill as complete, verify:
- All provider clients successfully authenticate with API keys
- Model router selects appropriate model based on task context
- Fallback mechanism tested with simulated provider failures
- Prompt templates render correctly with all variable types
- Cost tracking accurately captures token usage and calculates costs
- Streaming responses work for all supported providers
- Error messages from providers properly handled and logged
- Response format options (concise/detailed) function correctly
- Budget alerts trigger at configured thresholds
- All provider availability checks return accurate status
Failure Indicators
This skill has FAILED if:
- ❌ API authentication fails for any configured provider
- ❌ Model router throws errors during selection
- ❌ Fallback mechanism doesn't trigger on provider failure
- ❌ Prompt template variables not replaced ({{var}} remains)
- ❌ Cost calculations incorrect or missing
- ❌ Streaming responses incomplete or malformed
- ❌ Provider availability always returns false
- ❌ Token counts significantly mismatched with actual usage
- ❌ TypeScript compilation errors in provider interfaces
When NOT to Use
Do NOT use this skill when:
- Single-provider applications - If only using one AI provider, direct SDK integration is simpler
- No fallback requirements - Routing/fallback adds complexity you don't need
- Non-LLM AI systems - This skill is optimized for chat/completion APIs
- Synchronous-only workflows - If streaming not needed, simpler patterns exist
- No cost constraints - Cost tracking overhead unnecessary if budget unlimited
- Simple prompt-response - Template management overkill for static prompts
- Local-only models - Pattern designed for cloud API providers
Alternative approaches:
- Use provider SDK directly for single-provider apps
- Use LangChain/LlamaIndex for complex RAG pipelines
- Use Ollama client for local model hosting
Anti-Patterns (Avoid)
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Hardcoded API keys | Security vulnerability, key exposure | Use environment variables, secrets management |
| No timeout configuration | Hanging requests, resource waste | Set reasonable timeouts (30-120s) |
| Ignoring rate limits | API throttling, 429 errors | Implement exponential backoff, respect limits |
| Missing error context | Can't debug provider failures | Include provider name, model, timestamp in errors |
| Caching without TTL | Stale responses served indefinitely | Implement cache expiration (minutes to hours) |
| Over-aggressive fallback | Mask systematic provider issues | Log fallback events, monitor frequency |
| No cost budget limits | Runaway API costs | Implement per-period budget caps with alerts |
| Mixing sync/async patterns | Confusing error handling, deadlocks | Stick to async/await throughout |
| Generic prompt templates | Poor quality responses | Create task-specific templates with examples |
| No provider health checks | Fail on first request | Pre-check availability before critical operations |
Principles
This skill embodies these CODITECT principles:
- #2 First Principles - Abstracts provider differences to fundamental completion API
- #4 Separation of Concerns - Provider logic, routing, prompts, cost tracking separated
- #5 Eliminate Ambiguity - Model selection rules explicit and auditable
- #7 Error Handling Excellence - Fallback strategies prevent single points of failure
- #9 Observability First - Cost and usage tracking built-in from start
- #11 Security by Design - API keys externalized, no secrets in code
- #13 Automate Repetitive Tasks - Prompt templates eliminate copy-paste
Full Principles: CODITECT-STANDARD-AUTOMATION.md