# How AICallFn Enables Provider Neutrality in OpenMAIC Generation

> Learn how AICallFn provides provider neutrality in OpenMAIC generation. Effortlessly switch between AI providers like OpenAI and Anthropic without code changes.

- Repository: [MAIC/OpenMAIC](https://github.com/THU-MAIC/OpenMAIC)
- Tags: deep-dive
- Published: 2026-09-03

---

**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:

```ts
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.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/scene-generator.ts) and [`outline-generator.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/outline-generator.ts) contain zero provider-specific imports. All AI interaction happens through the injected function.

- **Runtime Injection** — Every generation function accepts `aiCall` as 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 `AICallFn` implementation, 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:

```ts
// 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:

```ts
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

```ts
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

```ts
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:

```ts
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`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/server/scene-generation.ts) instantiates the appropriate provider based on environment configuration:

```ts
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

- **`AICallFn`** in [`pipeline-types.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/pipeline-types.ts) defines a minimal, provider-agnostic contract for AI inference calls.
- All generation functions in [`scene-generator.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/scene-generator.ts), [`outline-generator.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/outline-generator.ts), and related files accept `aicall` as an injected parameter.
- Providers implement `AICallFn` to 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.