Skip to main content

Image Generation Implementation Guide

This guide provides detailed implementation examples for working with Bike4Mind's image generation system.

Model Configuration

Model Definitions

The system supports multiple image generation models with specific capabilities:

// b4m-core/packages/core/common/models.ts
export enum ImageModels {
// OpenAI Models
GPT_IMAGE_1 = 'gpt-image-1',

// BlackForest Labs Models
FLUX_PRO = 'flux-pro',
FLUX_PRO_1_1 = 'flux-pro-1.1',
FLUX_PRO_ULTRA = 'flux-pro-1.1-ultra',
FLUX_PRO_FILL = 'flux-pro-1.0-fill',
FLUX_PRO_CANNY = 'flux-pro-1.0-canny',
FLUX_PRO_DEPTH = 'flux-pro-1.0-depth',
FLUX_DEV = 'flux-dev',
FLUX_KONTEXT_PRO = 'flux-kontext-pro',
FLUX_KONTEXT_MAX = 'flux-kontext-max',
}

// Kontext models are image-to-image transformation models
export const KONTEXT_MODELS = [
ImageModels.FLUX_KONTEXT_PRO,
ImageModels.FLUX_KONTEXT_MAX,
] as const;

// Model constraints and supported sizes
export const IMAGE_SIZE_CONSTRAINTS = {
BFL: {
minWidth: 256,
maxWidth: 1440,
minHeight: 256,
maxHeight: 1440,
stepSize: 32,
defaultSize: '1280x960',
sizes: [
'1280x960', '1024x768', '800x600',
'1280x720', '1024x576', '1440x810',
'1024x1024', '768x768', '512x512',
'960x1280', '768x1024', '600x800',
] as const,
},
GPT_IMAGE_1: {
sizes: ['1024x1024', '1024x1536', '1536x1024'] as const,
defaultSize: '1024x1024',
},
} as const;

BFL Kontext Models (Image-to-Image Transformation)

Kontext models are specialized for image-to-image transformation and require an input image. The system implements a priority-based image selection:

Image Selection Priority

  1. First Priority: Images uploaded to the workbench (explicit user intent)
  2. Fallback: Most recent image from message history (convenience)
// Priority-based image selection logic
const fabFiles = await this.db.fabFiles.findAllInIds(fabFileIds || []);
let fileImage = fabFiles.find(file => file.mimeType.startsWith('image'));
let imageSource = 'workbench';

// Fallback to message history if no workbench image
if (!fileImage) {
const recentMessages = await this.db.quests.getMostRecentChatHistory(sessionId, 20);
const messageWithImage = recentMessages.find(msg => msg.images && msg.images.length > 0);

if (messageWithImage && messageWithImage.images) {
const mostRecentImageUrl = messageWithImage.images[0];
fileImage = {
filePath: mostRecentImageUrl,
mimeType: 'image/png' // Default, determined when processing
} as any;
imageSource = 'message_history';

console.log(`[DEBUG] Using image from message history:`, {
messageId: messageWithImage.id,
imageUrl: mostRecentImageUrl,
timestamp: messageWithImage.timestamp
});
}
}

// Process Kontext transformation
if (isKontextModel) {
if (!base64Image) {
throw new Error('Kontext models require an input image for transformation. Please either:\n1. Upload an image to the workbench, or\n2. Generate an image in this conversation first.');
}

const transformedImage = await service.transform(base64Image, prompt, {
model: model as ImageModels.FLUX_KONTEXT_PRO | ImageModels.FLUX_KONTEXT_MAX,
safety_tolerance,
prompt_upsampling,
seed,
output_format,
aspect_ratio,
});

images = [transformedImage];
}

Kontext Model Characteristics

  • API Endpoints: /v1/flux-kontext-pro and /v1/flux-kontext-max
  • Method: Uses transform() instead of generate()
  • Input: Requires input_image parameter (base64 encoded)
  • Output: Returns single transformed image per request
  • Dimensions: Uses aspect_ratio instead of pixel dimensions
  • Integration: Automatically detects and uses available images

User Experience

The UI adapts automatically for Kontext models:

// Check if current model is a Kontext model
const isKontextModel = model === ImageModels.FLUX_KONTEXT_PRO || model === ImageModels.FLUX_KONTEXT_MAX;

// Hide size controls for Kontext models
...(isKontextModel ? {} : {
label: 'Image Size',
type: 'select' as const,
value: size,
onChange: (value: OpenAIImageSize) => setLLM({ size: value }),
options: getAvailableSizes(model)
}),

// Show info message for Kontext models
{isKontextModel && (
<Alert variant="soft" color="info">
<Typography level="body-sm">
This model transforms existing images. Either upload an image to the workbench
or use a recently generated image from this conversation, then describe how
you want it changed.
</Typography>
</Alert>
)}

Parameter Validation

BFL models have specific safety tolerance requirements:

// b4m-core/packages/core/common/schemas/bfl.ts
export const BFL_SAFETY_TOLERANCE = {
MIN: 0,
MAX: 6,
DEFAULT: 4,
} as const;

// Validation schema
const BFLImageGenerationSchema = z.object({
prompt: z.string().min(1).max(2000),
safety_tolerance: z
.number()
.min(BFL_SAFETY_TOLERANCE.MIN)
.max(BFL_SAFETY_TOLERANCE.MAX)
.default(BFL_SAFETY_TOLERANCE.DEFAULT),
prompt_upsampling: z.boolean().default(false),
seed: z.number().nullable().optional(),
output_format: z.enum(['jpeg', 'png']).default('jpeg'),
width: z.number().min(256).max(1440).optional(),
height: z.number().min(256).max(1440).optional(),
aspect_ratio: z.string().optional(),
});

Frontend Implementation

LLM Context Integration

The LLM context manages all image generation state:

// packages/client/app/contexts/LLMContext.tsx
interface LLMContextProps {
// Model selection
model: string;

// OpenAI-specific parameters
size: OpenAIImageSize;
quality: OpenAIImageQuality;
style: OpenAIImageStyle;

// BFL-specific parameters
safety_tolerance: number | undefined;
prompt_upsampling: boolean | undefined;
width: number | undefined;
height: number | undefined;
aspect_ratio: string | undefined;

// Common parameters
seed: number | null | undefined;
output_format: 'jpeg' | 'png' | null | undefined;

// State management
setLLM: (params: Partial<LLMContextProps>) => void;
}

// Usage in components
const MyImageComponent = () => {
const { model, size, quality, setLLM } = useLLM();

const handleModelChange = (newModel: string) => {
setLLM({ model: newModel });
};

const handleSizeChange = (newSize: OpenAIImageSize) => {
setLLM({ size: newSize });
};

return (
<Select value={model} onChange={handleModelChange}>
{IMAGE_MODELS.map(model => (
<Option key={model} value={model}>{model}</Option>
))}
</Select>
);
};

Advanced AI Settings Component

The settings component dynamically shows controls based on the selected model:

// packages/client/app/components/Session/AdvancedAISettings.tsx
const AISettings: FC<AISettingsProps> = ({ /* props */ }) => {
const [model, size, quality, style, safety_tolerance, setLLM] = useLLM(
useShallow(s => [
s.model, s.size, s.quality, s.style,
s.safety_tolerance, s.setLLM
])
);

// Check if current model is an image model
const isImageModel = (model: string): model is ImageModelName => {
return IMAGE_MODELS.includes(model as ImageModelName);
};

// Get available sizes for current model
const getAvailableSizes = (model: string) => {
if (model === ImageModels.GPT_IMAGE_1) {
return IMAGE_SIZE_CONSTRAINTS.GPT_IMAGE_1.sizes;
} else if (BFL_IMAGE_MODELS.includes(model as any)) {
return IMAGE_SIZE_CONSTRAINTS.BFL.sizes;
}
return [];
};

// Model-specific settings
const imageSettings = useMemo(() => {
const settings = [
{
label: 'Image Size',
type: 'select' as const,
value: size || IMAGE_SIZE_CONSTRAINTS[getModelConstraintKey(model)].defaultSize,
onChange: (value: OpenAIImageSize | null) => value && setLLM({ size: value }),
options: getAvailableSizes(model).map(size => ({ value: size, label: size })),
},
{
label: 'Quality',
type: 'select' as const,
value: quality,
onChange: (value: OpenAIImageQuality | null) => value && setLLM({ quality: value }),
options: model === ImageModels.GPT_IMAGE_1
? [
{ value: 'low', label: 'Low' },
{ value: 'medium', label: 'Medium' },
{ value: 'high', label: 'High' },
{ value: 'auto', label: 'Auto' },
]
: [
{ value: 'standard', label: 'Standard' },
{ value: 'hd', label: 'HD' },
],
},
];

// Add BFL-specific settings
if (BFL_IMAGE_MODELS.includes(model as any)) {
settings.push({
label: 'Safety Tolerance',
type: 'slider' as const,
value: safety_tolerance ?? BFL_SAFETY_TOLERANCE.DEFAULT,
onChange: (value: number) => setLLM({ safety_tolerance: value }),
min: BFL_SAFETY_TOLERANCE.MIN,
max: BFL_SAFETY_TOLERANCE.MAX,
step: 1,
});
}

return settings;
}, [model, size, quality, safety_tolerance, setLLM]);

return (
<Box>
{/* Model toggle */}
<SwitchToggleGroup
options={[
{ value: 'text', label: 'Text Model', icon: <ChatIcon /> },
{ value: 'image', label: 'Image Model', icon: <ImageIcon /> },
]}
value={isImageModel(model) ? 'image' : 'text'}
onChange={handleModelChange}
/>

{/* Dynamic settings */}
{imageSettings.map(setting => (
<Box key={setting.label}>
<Typography>{setting.label}</Typography>
{setting.type === 'select' && (
<Select
value={setting.value}
onChange={(_, value) => setting.onChange(value)}
>
{setting.options.map(option => (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
))}
</Select>
)}
{setting.type === 'slider' && (
<Slider
value={setting.value}
min={setting.min}
max={setting.max}
step={setting.step}
onChange={(_, value) => setting.onChange(value as number)}
/>
)}
</Box>
))}
</Box>
);
};

Image Generation Command

Command handler for triggering image generation:

// packages/client/app/components/commands/ImageGenerationCommand.ts
export type ImageGenerationCommandArgs = {
params: string; // The prompt
currentSession: ISessionDocument;
workBenchFiles: IFabFileDocument[];
questId?: string; // For retrying failed generations
queryClient: QueryClient;
model: ImageModelName;
} & AIImageSettings;

export async function handleImageGenerationCommand(
args: ImageGenerationCommandArgs
): Promise<void> {
const {
params,
currentSession,
questId,
queryClient,
model,
workBenchFiles,
...imageSettings
} = args;

// Create optimistic update function
const optimisticOperation = questId
? (cb: () => Promise<IChatHistoryItemDocument>) =>
updateOptimisticQuest(queryClient, questId, { replies: [], images: [] }, cb)
: (cb: () => Promise<IChatHistoryItemDocument>) =>
createOptimisticQuest(queryClient, currentSession.id, params, cb);

const fabFileIds = workBenchFiles.map(file => file.id);

try {
await optimisticOperation(async () => {
const { data } = await api.post<IChatHistoryItemDocument>('/api/ai/generate-image', {
...imageSettings,
prompt: params,
sessionId: currentSession.id,
questId,
model,
fabFileIds,
});

return data;
});
} catch (error) {
console.error('Error generating image:', error);
toast.error('Failed to generate image');
}
}

Backend Implementation

API Endpoints

Generate Image Endpoint

// packages/client/pages/api/ai/generate-image.ts
import { baseApi } from '@server/middlewares/baseApi';
import { imageGeneration } from '@server/queueHandlers/imageGeneration';

const handler = baseApi().post(async (req, res) => {
const quest = await imageGeneration.invoke({
userId: req.user.id,
body: req.body,
});

return res.json(quest);
});

export const config = {
api: {
externalResolver: true,
},
};

export default handler;

Edit Image Endpoint

// packages/client/pages/api/ai/edit-image.ts
import { baseApi } from '@server/middlewares/baseApi';
import { imageEdit } from '@server/queueHandlers/imageEdit';

const handler = baseApi().post(async (req, res) => {
const quest = await imageEdit.invoke({
userId: req.user.id,
body: req.body,
});

return res.json(quest);
});

export default handler;

Service Implementation

Image Generation Service

// b4m-core/packages/core/services/llm/ImageGeneration.ts
export class ImageGenerationService {
constructor(private options: IImageGenerationServiceOptions) {}

async invoke({ body, userId }: {
body: z.infer<typeof GenerateImageRequestBodySchema>;
userId: string
}) {
const { sessionId, prompt, model, questId, fabFileIds, ...rest } =
GenerateImageRequestBodySchema.parse(body);

const session = await this.db.sessions.findById(sessionId);
if (!session) throw new NotFoundError('Session not found');

// Create or update quest
let quest;
if (questId) {
quest = await this.db.quests.findById(questId);
if (!quest) throw new NotFoundError('Quest not found');
quest.images = [];
quest.replies = [];
quest.status = undefined;
await this.db.quests.update(quest);
} else {
quest = await this.db.quests.create({
sessionId,
prompt,
type: 'message',
timestamp: new Date(),
replies: [],
promptMeta: {
model: { name: model, parameters: {} },
session: { id: sessionId, userId },
},
});
}

// Queue for background processing
await this.startImageGenerationProcess({
...rest,
sessionId,
questId: quest.id,
userId,
prompt,
model,
fabFileIds,
});

return quest;
}

async process({ body, logger }: {
body: z.infer<typeof ImageGenerationBodySchema>;
logger: Logger
}) {
const {
sessionId, questId, userId, prompt, model, n = 1,
width, height, size, quality, style,
safety_tolerance, prompt_upsampling, seed, output_format,
aspect_ratio, fabFileIds,
} = ImageGenerationBodySchema.parse(body);

logger.updateMetadata({ notebookId: sessionId, questId, userId });

const quest = await this.db.quests.findById(questId);
if (!quest) throw new NotFoundError('Quest not found');

quest.status = 'running';
await this.db.quests.update(quest);

const user = await this.db.users.findById(userId);
if (!user) throw new NotFoundError('User not found');

// Get API keys
const apiKeyTable = await getEffectiveLLMApiKeys(userId, { db: this.db });

// WebSocket progress updates
const clientMessageSender = new ClientMessageSender(this.db, logger);
const wsEndpoint = this.wsHttpsUrl;

const parseQuestToStreamPayload = (quest: IChatHistoryItemDocument) => ({
id: questId,
sessionId: sessionId,
reply: quest.reply,
replies: quest.replies,
type: quest.type,
status: quest.status,
images: quest.images,
});

try {
// Send progress update
await clientMessageSender.sendToClient(userId, wsEndpoint, {
action: 'streamed_chat_completion',
quest: parseQuestToStreamPayload(quest),
statusMessage: 'Now painting...',
});

// Process fab files for image variation
const fabFiles = await this.db.fabFiles.findAllInIds(fabFileIds || []);
const fileImage = fabFiles.find(file => file.mimeType.startsWith('image'));

// Select appropriate service
const isBFLModel = BFL_IMAGE_MODELS.includes(model as any);
const service = isBFLModel
? aiImageService('bfl', apiKeyTable.bfl!, logger)
: aiImageService('openai', apiKeyTable.openai!, logger);

let images: string[] = [];

if (isBFLModel) {
// BFL-specific processing
const isBFLUltraModel = model === ImageModels.FLUX_PRO_ULTRA;
const bflModel = isBFLUltraModel ? ImageModels.FLUX_PRO_ULTRA : ImageModels.FLUX_PRO;

const signedUrl = fileImage?.filePath
? await this.fabFileStorage.getSignedUrl(fileImage.filePath)
: undefined;
const base64Image = signedUrl ? await imageUrlToBase64(signedUrl) : undefined;

if (isBFLUltraModel) {
// Ultra models use aspect ratios
images = await service.generate(prompt, {
width: undefined,
height: undefined,
aspect_ratio: aspect_ratio || '16:9',
user: userId,
model: bflModel,
safety_tolerance,
prompt_upsampling,
image_prompt: base64Image,
seed,
output_format,
size: null,
n,
});
} else {
// Pro models use width/height
images = await service.generate(prompt, {
width: width || 1024,
height: height || 768,
user: userId,
model: bflModel,
safety_tolerance,
image_prompt: base64Image,
prompt_upsampling,
seed,
output_format,
size: null,
n,
});
}
} else {
// OpenAI-specific processing
const openAiSize = size && size !== '1024x768' ? size : undefined;
const signedUrl = fileImage?.filePath
? await this.fabFileStorage.getSignedUrl(fileImage.filePath)
: undefined;

const openaiParams: any = {
model: model as any,
n,
size: openAiSize,
imagePrompt: signedUrl,
user: userId,
};

// Handle GPT-Image-1 parameter differences
if (model === ImageModels.GPT_IMAGE_1) {
if (quality) {
openaiParams.quality = quality === 'standard' ? 'medium' :
quality === 'hd' ? 'high' : quality;
}
} else {
openaiParams.quality = quality;
openaiParams.style = style;
openaiParams.response_format = 'url';
}

images = await service.generate(prompt, openaiParams);
}

// Upload images to S3
await clientMessageSender.sendToClient(userId, wsEndpoint, {
action: 'streamed_chat_completion',
quest: parseQuestToStreamPayload(quest),
statusMessage: 'Tucking your image into storage...',
});

const imagePaths = await Promise.all(
images.map(async (image, index) => {
const buffer = await downloadImage(image);
const fileType = await fileTypeFromBuffer(buffer);
const filename = `${uuidv4()}.${fileType?.ext}`;

return await this.storage.upload(buffer, filename, {
ACL: 'public-read',
});
})
);

const publicUrls = imagePaths.map(path => this.storage.getPublicUrl(path));

// Update quest with results
quest.reply = '';
quest.replies = [];
quest.images = publicUrls;
quest.status = 'done';
await this.db.quests.update(quest);

// Final progress update
await clientMessageSender.sendToClient(userId, wsEndpoint, {
action: 'streamed_chat_completion',
quest: parseQuestToStreamPayload(quest),
statusMessage: null,
});

} catch (error) {
logger.error('Image generation failed:', error);
quest.status = 'error';
await this.db.quests.update(quest);

await clientMessageSender.sendToClient(userId, wsEndpoint, {
action: 'streamed_chat_completion',
quest: parseQuestToStreamPayload(quest),
statusMessage: 'Image generation failed',
});

throw error;
}
}
}

Provider Implementation

OpenAI Image Service

// b4m-core/packages/core/utils/imageGeneration/OpenAIImageService.ts
export class OpenAIImageService extends AIImageService {
async generate(prompt: string, options: OpenAIImageGenerationOptions): Promise<string[]> {
const openai = new OpenAI({ apiKey: this.apiKey });

const {
safety_tolerance,
prompt_upsampling,
seed: bflSeed,
output_format,
imagePrompt,
...openaiOptions
} = options;

// Handle GPT-Image-1 differences
if (options.model === ImageModels.GPT_IMAGE_1) {
delete openaiOptions.style;
delete openaiOptions.response_format;

if (openaiOptions.quality) {
if (openaiOptions.quality === 'standard') {
openaiOptions.quality = 'medium' as any;
} else if (openaiOptions.quality === 'hd') {
openaiOptions.quality = 'high' as any;
}
}
} else {
openaiOptions.response_format = 'url';
}

// Handle seed parameter
if (bflSeed !== null && bflSeed !== undefined) {
(openaiOptions as any).seed = bflSeed;
}

let images: string[] = [];

// Legacy model handling (no longer needed)
if (false) {
await Promise.all(
Array.from({ length: options.n ?? 1 }).map(async () => {
const result = await openai.images.generate({
prompt,
...openaiOptions,
n: 1,
});
images.push(...this.imageResponseToUrl(result));
})
);
} else {
let result;

if (imagePrompt) {
// Handle image variations
const localImagePath = await downloadImage(imagePrompt, 'downloaded_image.jpg');
const imageStream = createReadStream(localImagePath);
const { style, quality, ...opts } = openaiOptions;

result = await openai.images.createVariation({
...opts,
image: imageStream,
size: ['256x256', '512x512', '1024x1024'].find(s => s === openaiOptions.size) as
| '256x256' | '512x512' | '1024x1024',
});
} else {
result = await openai.images.generate({
prompt,
...openaiOptions,
});
}

images = this.imageResponseToUrl(result);
}

return images;
}

private imageResponseToUrl(response: OpenAI.Images.ImagesResponse): string[] {
return (response?.data ?? []).map(imageData => {
// Handle both URL and base64 responses
if (imageData.b64_json) {
return `data:image/png;base64,${imageData.b64_json}`;
}

if (imageData.url) {
return imageData.url;
}

throw new Error(`Invalid image response format`);
});
}
}

BFL Image Service

// b4m-core/packages/core/utils/imageGeneration/BFLImageService.ts
export class BFLImageService extends AIImageService {
private baseUrl = 'https://api.us1.bfl.ai/v1';

async generate(prompt: string, options: BFLGenerationOptions): Promise<string[]> {
const {
n = 1,
user = 'user',
model = ImageModels.FLUX_PRO,
safety_tolerance = 0.5,
prompt_upsampling = false,
seed = null,
output_format = 'png',
width = 1024,
height = 768,
aspect_ratio = '16:9',
image_prompt,
...modelSpecificOptions
} = options;

// Generate n images in parallel
const urls = await Promise.all(
Array.from({ length: n }).map(async () => {
const requestBody: any = {
prompt,
safety_tolerance,
prompt_upsampling,
seed,
image_prompt,
output_format: output_format || 'jpeg',
user,
...modelSpecificOptions,
};

// Model-specific parameters
if (model.includes('ultra')) {
// Ultra models use aspect ratios
if (aspect_ratio) {
requestBody.aspect_ratio = aspect_ratio;
}
} else {
// Pro models use width/height
requestBody.width = width;
requestBody.height = height;
}

// Submit generation request
const submitResponse = await axios.post(`${this.baseUrl}/${model}`, requestBody, {
headers: {
accept: 'application/json',
'x-key': this.apiKey,
'Content-Type': 'application/json',
},
});

const requestId = submitResponse.data.id;

// Poll for result
return await this.pollForResult(requestId);
})
);

return urls;
}

private async pollForResult(requestId: string, maxAttempts = 60, interval = 2000): Promise<string> {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const response = await axios.get(`${this.baseUrl}/get_result`, {
params: { id: requestId },
headers: {
accept: 'application/json',
'x-key': this.apiKey,
},
});

const { status, result } = response.data;

if (status === 'Ready' && result?.sample) {
return result.sample;
}

if (status === 'Request Moderated') {
throw new Error('Content moderated - adjust safety tolerance or prompt');
}

if (status === 'Failed') {
throw new Error(`Generation failed: ${result?.error || 'Unknown error'}`);
}

// Wait before next poll
await new Promise(resolve => setTimeout(resolve, interval));
}

throw new Error(`Generation timed out after ${maxAttempts} attempts`);
}
}

Usage Examples

Basic Image Generation

const generateImage = async () => {
const { model, size, quality, safety_tolerance } = useLLM();

await handleImageGenerationCommand({
params: "A majestic mountain landscape at sunset",
currentSession,
workBenchFiles: [],
queryClient,
model: model as ImageModelName,
size,
quality,
safety_tolerance,
n: 1,
});
};

Image Editing

const editImage = async (sourceImage: string, maskFile: File) => {
// Upload mask as FabFile
const maskFabFile = await createFabFileOnServerWithUpload({
type: KnowledgeType.FILE,
fileName: `mask_${Date.now()}.png`,
mimeType: 'image/png',
fileSize: maskFile.size,
}, maskFile);

await handleImageEditCommand({
params: "Replace the sky with a starry night",
image: sourceImage,
currentSession,
workBenchFiles: [maskFabFile],
queryClient,
model: ImageModels.FLUX_PRO_FILL,
});
};

Model-Specific Configuration

const configureForModel = (model: ImageModelName) => {
const { setLLM } = useLLM();

if (model === ImageModels.FLUX_PRO_ULTRA) {
setLLM({
model,
aspect_ratio: '16:9',
safety_tolerance: 4,
prompt_upsampling: true,
output_format: 'png',
});
} else if (false) { // Legacy model support removed
setLLM({
model,
size: '1024x1024',
quality: 'hd',
style: 'vivid',
});
} else if (model === ImageModels.GPT_IMAGE_1) {
setLLM({
model,
size: '1024x1024',
quality: 'high', // Note: different quality values
});
}
};

This implementation guide provides the foundation for working with the image generation system. For specific use cases or custom implementations, refer to the existing codebase and follow these established patterns.