How AICallFn Enables Provider Neutrality in OpenMAIC Generation
AICallFn is a single injectable function type that decouples OpenMAIC's generation logic from any specific AI provider, allowing seamless swapping between OpenAI, Anthropic, or custom implementations without modifying core generation code.
OpenMAIC's generation architecture is intentionally built around the AICallFn abstraction, defined in packages/@openmaic/generation/src/pipeline-types.ts. This lightweight TypeScript type represents a contract for AI inference calls, enabling the entire generation pipeline—from scene creation to outline generation—to remain completely provider-agnostic.
What Is AICallFn?
The AICallFn type specifies a minimal, predictable interface:
export type AICallFn = (
systemPrompt: string,
userPrompt: string,
images?: readonly string[],
) => Promise<string>;
This signature captures everything a generation pipeline needs: system context, user input, optional visual content, and a string response. Notably absent are provider-specific parameters like temperature, top_p, or authentication tokens—those concerns live entirely within the implementation.
By standardizing on this contract, OpenMAIC's generators never import SDKs, manage API keys, or handle provider quirks. They simply invoke aiCall and process the returned string.
How Provider Neutrality Is Achieved
The AICallFn abstraction delivers provider neutrality through four key mechanisms:
-
Complete Decoupling — Core generation files like
scene-generator.tsandoutline-generator.tscontain zero provider-specific imports. All AI interaction happens through the injected function. -
Runtime Injection — Every generation function accepts
aiCallas a parameter. The caller decides which implementation to supply, making provider selection an external configuration concern. -
Uniform Response Handling — Generators parse the string response consistently, whether it originates from GPT-4, Claude, or a local model. JSON parsing, error handling, and validation operate on standard strings.
-
Isolation of Provider Complexity — Retry logic, token counting, streaming, and multimodal formatting remain encapsulated in the
AICallFnimplementation, not scattered through generation code.
Core Generation Files Using AICallFn
Scene Generation
In packages/@openmaic/generation/src/scene-generator.ts, the generateSceneContent function receives aiCall and uses it to produce structured scene content:
// Simplified from scene-generator.ts
export async function generateSceneContent(params: {
systemPrompt: string;
userPrompt: string;
aiCall: AICallFn;
// ... other params
}): Promise<SceneContent> {
const rawResponse = await params.aiCall(
params.systemPrompt,
params.userPrompt
);
// Parse and validate the JSON response
return parseSceneContent(rawResponse);
}
The same pattern appears in generateSceneActions, generateSceneWithResources, and other scene-related functions—all delegating to the injected aiCall.
Outline Generation
packages/@openmaic/generation/src/outline-generator.ts extends the pattern to course or presentation outlines:
export async function generateOutline(params: {
systemPrompt: string;
userPrompt: string;
aiCall: AICallFn;
}): Promise<Outline> {
const response = await params.aiCall(
params.systemPrompt,
params.userPrompt
);
return parseOutline(response);
}
This consistency across generators means switching providers requires changing only the aiCall implementation passed at the call site.
Implementing Custom AICallFn Providers
OpenAI Implementation
import { OpenAI } from 'openai';
import type { AICallFn } from '@openmaic/generation';
const openAICall: AICallFn = async (systemPrompt, userPrompt, images = []) => {
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userPrompt },
];
// Append images if provided using GPT-4 Vision format
if (images.length > 0) {
messages[1].content = [
{ type: 'text', text: userPrompt },
...images.map(url => ({
type: 'image_url' as const,
image_url: { url },
})),
];
}
const completion = await client.chat.completions.create({
model: 'gpt-4o',
messages,
temperature: 0.7,
});
return completion.choices[0].message.content ?? '';
};
Anthropic Implementation
import Anthropic from '@anthropic-ai/sdk';
import type { AICallFn } from '@openmaic/generation';
const anthropicCall: AICallFn = async (systemPrompt, userPrompt, images = []) => {
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
const content: Anthropic.ContentBlock[] = [
{ type: 'text', text: userPrompt },
];
// Append images using Anthropic's base64 or URL format
if (images.length > 0) {
for (const imageUrl of images) {
content.push({
type: 'image',
source: {
type: 'url',
url: imageUrl,
},
});
}
}
const response = await client.messages.create({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 4096,
system: systemPrompt,
messages: [{ role: 'user', content }],
});
// Extract text from content blocks
return response.content
.filter((block): block is Anthropic.TextBlock => block.type === 'text')
.map(block => block.text)
.join('');
};
Both implementations satisfy AICallFn and can be used interchangeably with any OpenMAIC generator.
Testing with Mock AICallFn
The abstraction enables deterministic, fast unit tests without network calls or API keys:
import { vi } from 'vitest';
import { generateSceneContent, type AICallFn } from '@openmaic/generation';
const mockSceneResponse = JSON.stringify({
title: 'Introduction to Climate',
slides: [
{ id: '1', title: 'What is Climate?', content: 'Climate is...' },
],
});
const mockAiCall: AICallFn = vi.fn().mockResolvedValue(mockSceneResponse);
const result = await generateSceneContent({
systemPrompt: 'You are an educational content creator.',
userPrompt: 'Create a scene about climate basics.',
aiCall: mockAiCall,
});
// Verify the function was called with correct arguments
expect(mockAiCall).toHaveBeenCalledWith(
'You are an educational content creator.',
'Create a scene about climate basics.',
undefined
);
// Verify parsed output structure
expect(result.slides).toHaveLength(1);
Test files throughout tests/generation/ demonstrate this pattern, mocking AICallFn to validate prompt construction and response parsing in isolation.
Server-Side Integration
Production deployments wire AICallFn implementations in server-side code. For example, lib/server/scene-generation.ts instantiates the appropriate provider based on environment configuration:
import { generateSceneContent, type AICallFn } from '@openmaic/generation';
function createAICallFn(provider: 'openai' | 'anthropic'): AICallFn {
switch (provider) {
case 'openai':
return createOpenAICall();
case 'anthropic':
return createAnthropicCall();
default:
throw new Error(`Unknown provider: ${provider}`);
}
}
// API route handler
export async function handleSceneGeneration(request: Request) {
const { systemPrompt, userPrompt, provider } = await request.json();
const aiCall = createAICallFn(provider);
const scene = await generateSceneContent({
systemPrompt,
userPrompt,
aiCall, // Provider-specific implementation injected here
});
return Response.json(scene);
}
This architecture allows runtime provider selection through configuration, not code changes.
Summary
AICallFninpipeline-types.tsdefines a minimal, provider-agnostic contract for AI inference calls.- All generation functions in
scene-generator.ts,outline-generator.ts, and related files acceptaicallas an injected parameter. - Providers implement
AICallFnto encapsulate SDK-specific logic, authentication, and response formatting. - The same generation code works with OpenAI, Anthropic, local models, or mocked implementations without modification.
- Tests leverage the abstraction for deterministic, fast verification of generation logic.
Frequently Asked Questions
What parameters does AICallFn accept?
AICallFn accepts three parameters: systemPrompt (string), userPrompt (string), and optionally images (readonly array of strings). It returns a Promise<string>. This minimal signature keeps the contract simple while supporting text and multimodal use cases.
Can I add custom parameters like temperature or max_tokens?
Provider-specific parameters are configured in your AICallFn implementation, not the type itself. Wrap your SDK call with your preferred defaults, or accept additional configuration through closure variables when constructing the function.
How do I add a new AI provider to OpenMAIC?
Create a function matching the AICallFn signature that calls your provider's SDK and returns a string. Pass this function to any OpenMAIC generator. No changes to core generation code are required. See the OpenAI and Anthropic examples above for reference patterns.
Why does OpenMAIC use a function type instead of an interface or class?
A function type minimizes boilerplate and enables lightweight implementations. Any function satisfying the signature works—arrow functions, methods, or wrapped SDK calls—without requiring class instantiation or interface implementation ceremonies.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →