Responses updates.

This commit is contained in:
jrmyr 2025-04-27 19:28:50 +00:00
parent c533f74b32
commit 2eeb70b114
2 changed files with 328 additions and 154 deletions

View File

@ -1,5 +1,8 @@
// _opt/responses.js /**
// Simplified OpenAI Responses module with clear flow and context management * Simplified OpenAI Responses module for Discord.
* Listens to message events, sends chat queries to the OpenAI Responses API,
* and handles text or image (function_call) outputs.
*/
import fs from 'fs/promises'; import fs from 'fs/promises';
import path from 'path'; import path from 'path';
import { OpenAI } from 'openai'; import { OpenAI } from 'openai';
@ -10,10 +13,10 @@ import { AttachmentBuilder } from 'discord.js';
const MAX_DISCORD_MSG_LENGTH = 2000; const MAX_DISCORD_MSG_LENGTH = 2000;
/** /**
* Split a long message into chunks <= maxLength, preserving code fences by closing and reopening. * Split a long message into chunks of at most maxLength, preserving code fences.
* @param {string} text * @param {string} text - Full message text to split.
* @param {number} maxLength * @param {number} maxLength - Maximum length per chunk.
* @returns {string[]} * @returns {string[]} Array of message chunks.
*/ */
function splitMessage(text, maxLength = MAX_DISCORD_MSG_LENGTH) { function splitMessage(text, maxLength = MAX_DISCORD_MSG_LENGTH) {
const lines = text.split(/\n/); const lines = text.split(/\n/);
@ -68,7 +71,10 @@ function splitMessage(text, maxLength = MAX_DISCORD_MSG_LENGTH) {
} }
/** /**
* Load the system prompt from disk. * Load AI system prompt text from a file.
* @param {string} filePath - Path to the prompt file.
* @param {object} logger - Logger instance for reporting.
* @returns {Promise<string>} Promise resolving to the prompt text or empty string.
*/ */
async function loadSystemPrompt(filePath, logger) { async function loadSystemPrompt(filePath, logger) {
try { try {
@ -82,9 +88,12 @@ async function loadSystemPrompt(filePath, logger) {
} }
/** /**
* Determine if the bot should respond: * Determine whether the bot should respond to a message.
* - Mentioned * Triggers when the bot is mentioned or when the message is a direct reply.
* - Direct reply * @param {Message} message - The incoming Discord message.
* @param {string} botId - The bot user ID.
* @param {object} logger - Logger for debugging.
* @returns {Promise<boolean>} True if the bot should respond.
*/ */
async function shouldRespond(message, botId, logger) { async function shouldRespond(message, botId, logger) {
if (message.author.bot || !botId) return false; if (message.author.bot || !botId) return false;
@ -101,14 +110,22 @@ async function shouldRespond(message, botId, logger) {
} }
/** /**
* Cache the last response ID for context continuity. * Cache the last AI response ID for a channel or thread, for context continuity.
* @param {object} client - Discord client with pocketbase cache.
* @param {string} key - Channel or thread identifier.
* @param {string} id - The AI response ID to cache.
* @param {number} ttlSeconds - Time-to-live for the cache entry in seconds.
*/ */
function cacheResponse(client, key, id, ttlSeconds) { function cacheResponse(client, key, id, ttlSeconds) {
client.pb?.cache?.set(key, id, ttlSeconds); client.pb?.cache?.set(key, id, ttlSeconds);
} }
/** /**
* Award output tokens via the scorekeeper module. * Award token usage points to a user for AI output via the scorekeeper.
* @param {object} client - Discord client with scorekeeper.
* @param {string} guildId - ID of the guild (server).
* @param {string} userId - ID of the user.
* @param {number} amount - Number of tokens to award.
*/ */
function awardOutput(client, guildId, userId, amount) { function awardOutput(client, guildId, userId, amount) {
if (client.scorekeeper && amount > 0) { if (client.scorekeeper && amount > 0) {
@ -118,8 +135,13 @@ function awardOutput(client, guildId, userId, amount) {
} }
/** /**
* Handle image generation function calls. * Handle an OpenAI 'generate_image' function call response.
* Returns true if an image was handled and replied. * Downloads generated images, saves to disk, and replies with attachments.
* @param {object} client - Discord client instance.
* @param {Message} message - The original Discord message to reply to.
* @param {object} resp - OpenAI Responses API response containing function_call.
* @param {object} cfg - Module configuration for image generation.
* @returns {Promise<boolean>} True if the function call was handled.
*/ */
async function handleImage(client, message, resp, cfg) { async function handleImage(client, message, resp, cfg) {
const calls = Array.isArray(resp.output) ? resp.output : []; const calls = Array.isArray(resp.output) ? resp.output : [];
@ -132,69 +154,91 @@ async function handleImage(client, message, resp, cfg) {
await message.reply('Cannot generate image: empty prompt.'); await message.reply('Cannot generate image: empty prompt.');
return true; return true;
} }
// Determine image size based on aspect: square, landscape, or portrait // Use image model defined in config
// Square will always use 1024x1024 const model = cfg.imageGeneration.defaultModel;
let size; const promptText = args.prompt;
switch (args.aspect) { // Determine number of images (1-10); DALL·E-3 only supports 1
case 'landscape': let count = 1;
size = '1792x1024'; if (args.n != null) {
break; const nVal = typeof args.n === 'number' ? args.n : parseInt(args.n, 10);
case 'portrait': if (!Number.isNaN(nVal)) count = nVal;
size = '1024x1792';
break;
case 'square':
size = '1024x1024';
break;
default:
size = '1024x1024';
} }
// Determine image quality, defaulting to cfg.imageGeneration.defaultQuality // clamp between 1 and 10
const quality = ['standard', 'hd'].includes(args.quality) count = Math.max(1, Math.min(10, count));
? args.quality if (model === 'dall-e-3') count = 1;
: cfg.imageGeneration.defaultQuality; const size = args.size || 'auto';
// Determine quality based on config and model constraints
let quality = args.quality || cfg.imageGeneration.defaultQuality;
if (model === 'gpt-image-1') {
if (!['low', 'medium', 'high', 'auto'].includes(quality)) quality = 'auto';
} else if (model === 'dall-e-2') {
quality = 'standard';
} else if (model === 'dall-e-3') {
if (!['standard', 'hd', 'auto'].includes(quality)) quality = 'standard';
}
const background = args.background;
const moderation = args.moderation;
const outputFormat = args.output_format;
const compression = args.output_compression;
const style = args.style;
const user = args.user || message.author.id;
try { try {
// Generate image via OpenAI // Build generate parameters
const imgRes = await client.openai.images.generate({ model: 'dall-e-3', prompt: args.prompt, quality: quality, size: size, n: 1 }); const genParams = { model, prompt: promptText, n: count, size, quality, user };
const url = imgRes.data?.[0]?.url; // response_format supported for DALL·E models (not gpt-image-1)
if (!url) throw new Error('No image URL'); if (model !== 'gpt-image-1' && args.response_format) {
// Download and save locally genParams['response_format'] = args.response_format;
const dl = await axios.get(url, { responseType: 'arraybuffer' }); }
const buf = Buffer.from(dl.data); // gpt-image-1 supports background, moderation, output_format, and output_compression
const filename = `${message.author.id}-${Date.now()}.png`; if (model === 'gpt-image-1') {
const dir = cfg.imageGeneration.imageSavePath || './images'; if (background) genParams['background'] = background;
if (moderation) genParams['moderation'] = moderation;
if (outputFormat) {
genParams['output_format'] = outputFormat;
// only support compression for JPEG or WEBP formats
if (['jpeg','webp'].includes(outputFormat) && typeof compression === 'number') {
genParams['output_compression'] = compression;
}
}
}
// dall-e-3 supports style
if (model === 'dall-e-3' && style) {
genParams['style'] = style;
}
// Generate images via OpenAI Images API
const imgRes = await client.openai.images.generate(genParams);
const images = imgRes.data || [];
if (!images.length) throw new Error('No images generated');
// Ensure save directory exists
const dir = cfg.imageGeneration?.imageSavePath || './images';
await fs.mkdir(dir, { recursive: true }); await fs.mkdir(dir, { recursive: true });
const filePath = path.join(dir, filename); const attachments = [];
await fs.writeFile(filePath, buf); const outputs = [];
client.logger.info(`Saved image: ${filePath}`); // Process each generated image
// Reply with attachment for (let i = 0; i < images.length; i++) {
const attachment = new AttachmentBuilder(buf, { name: filename }); const img = images[i];
await message.reply({ content: args.prompt, files: [attachment] }); let buffer, ext = outputFormat || 'png';
// Follow-up recap to preserve conversation context (submit function tool output) if (img.b64_json) {
try { buffer = Buffer.from(img.b64_json, 'base64');
const convKey = message.thread?.id || message.channel.id; outputs.push({ b64_json: img.b64_json });
// Build a function_call_output input item for the Responses API } else if (img.url) {
const toolOutputItem = { const dl = await axios.get(img.url, { responseType: 'arraybuffer' });
type: 'function_call_output', buffer = Buffer.from(dl.data);
call_id: fn.call_id, // derive extension from URL if possible
output: JSON.stringify({ url }), const parsed = path.extname(img.url.split('?')[0]).replace(/^[.]/, '');
}; if (parsed) ext = parsed;
const recapBody = { outputs.push({ url: img.url });
model: cfg.defaultModel, } else {
// re-use original system/developer instructions throw new Error('No image data');
instructions: client.responsesSystemPrompt,
previous_response_id: resp.id,
input: [toolOutputItem],
max_output_tokens: Math.min(100, cfg.defaultMaxTokens),
temperature: cfg.defaultTemperature,
};
const recapResp = await client.openai.responses.create(recapBody);
cacheResponse(client, convKey, recapResp.id, Math.floor(cfg.conversationExpiry / 1000));
// Award tokens for the recap chat response
const recapTokens = recapResp.usage?.total_tokens ?? recapResp.usage?.completion_tokens ?? 0;
awardOutput(client, message.guild.id, message.author.id, recapTokens);
} catch (err) {
client.logger.error(`Recap failed: ${err.message}`);
} }
const filename = `${message.author.id}-${Date.now()}-${i}.${ext}`;
const filePath = path.join(dir, filename);
await fs.writeFile(filePath, buffer);
client.logger.info(`Saved image: ${filePath}`);
attachments.push(new AttachmentBuilder(buffer, { name: filename }));
}
// Reply with attachments
await message.reply({ content: promptText, files: attachments });
} catch (err) { } catch (err) {
client.logger.error(`Image error: ${err.message}`); client.logger.error(`Image error: ${err.message}`);
await message.reply(`Image generation error: ${err.message}`); await message.reply(`Image generation error: ${err.message}`);
@ -203,12 +247,11 @@ async function handleImage(client, message, resp, cfg) {
} }
/** /**
* Main message handler: * Main message handler for Discord 'messageCreate' events.
* 1. Determine if bot should respond * Sends user messages to the AI, handles text or image responses, and awards tokens.
* 2. Build and send AI request * @param {object} client - Discord client instance.
* 3. Cache response ID * @param {object} cfg - Module configuration.
* 4. Handle image or text reply * @param {Message} message - Incoming Discord message.
* 5. Award output points
*/ */
async function onMessage(client, cfg, message) { async function onMessage(client, cfg, message) {
const logger = client.logger; const logger = client.logger;
@ -252,19 +295,58 @@ async function onMessage(client, cfg, message) {
// Assemble any enabled tools // Assemble any enabled tools
const tools = []; const tools = [];
if (cfg.tools?.imageGeneration) { if (cfg.tools?.imageGeneration) {
const model = cfg.imageGeneration.defaultModel;
// Configure allowed sizes per model
let sizeEnum;
switch (model) {
case 'gpt-image-1': sizeEnum = ['auto','1024x1024','1536x1024','1024x1536']; break;
case 'dall-e-2': sizeEnum = ['256x256','512x512','1024x1024']; break;
case 'dall-e-3': sizeEnum = ['auto','1024x1024','1792x1024','1024x1792']; break;
default: sizeEnum = ['auto','1024x1024'];
}
// Configure quality options per model
let qualityEnum;
switch (model) {
case 'gpt-image-1': qualityEnum = ['auto','low','medium','high']; break;
case 'dall-e-2': qualityEnum = ['standard']; break;
case 'dall-e-3': qualityEnum = ['auto','standard','hd']; break;
default: qualityEnum = ['auto','standard'];
}
// Build schema properties dynamically
const properties = {
prompt: { type: 'string', description: 'Text description of desired image(s).' },
n: { type: 'number', description: 'Number of images to generate.' },
size: { type: 'string', enum: sizeEnum, description: 'Image size.' },
quality: { type: 'string', enum: qualityEnum, description: 'Image quality.' },
user: { type: 'string', description: 'Unique end-user identifier.' }
};
if (model !== 'gpt-image-1') {
properties.response_format = { type: 'string', enum: ['url','b64_json'], description: 'Format of returned images.' };
}
if (model === 'gpt-image-1') {
properties.background = { type: 'string', enum: ['transparent','opaque','auto'], description: 'Background transparency.' };
properties.moderation = { type: 'string', enum: ['low','auto'], description: 'Content moderation level.' };
properties.output_format = { type: 'string', enum: ['png','jpeg','webp'], description: 'Output image format.' };
properties.output_compression = { type: 'number', description: 'Compression level (0-100).' };
}
if (model === 'dall-e-3') {
properties.style = { type: 'string', enum: ['vivid','natural'], description: 'Style option for dall-e-3.' };
}
// Determine required fields
const required = ['prompt','n','size','quality','user'];
if (model !== 'gpt-image-1') required.push('response_format');
if (model === 'gpt-image-1') required.push('background','moderation','output_format','output_compression');
if (model === 'dall-e-3') required.push('style');
// Register the function tool
tools.push({ tools.push({
type: 'function', type: 'function',
name: 'generate_image', name: 'generate_image',
description: 'Generate an image with a given prompt, aspect, and quality.', description: `Generate images using model ${model} with requested parameters.`,
parameters: { parameters: {
type: 'object', type: 'object',
properties: { properties,
prompt: { type: 'string' }, required,
aspect: { type: 'string', enum: ['square','portrait','landscape'] }, additionalProperties: false
quality: { type: 'string', enum: ['standard', 'hd'] },
},
required: ['prompt','aspect','quality'],
additionalProperties: false,
}, },
strict: true strict: true
}); });
@ -346,11 +428,11 @@ async function onMessage(client, cfg, message) {
} }
/** /**
* Send a narrative response to a specific Discord channel or thread. * Send a standalone narrative via the Responses API to a Discord channel or thread.
* @param {import('discord.js').Client} client - Discord client instance. * @param {object} client - Discord client instance.
* @param {Object} cfg - Responses module configuration. * @param {object} cfg - Module configuration.
* @param {string} channelId - ID of the Discord channel or thread. * @param {string} channelId - Target channel or thread ID.
* @param {string} text - Narrative input text to process. * @param {string} text - Narrative prompt text.
*/ */
export async function sendNarrative(client, cfg, channelId, text) { export async function sendNarrative(client, cfg, channelId, text) {
const logger = client.logger; const logger = client.logger;
@ -387,7 +469,12 @@ export async function sendNarrative(client, cfg, channelId, text) {
} }
/** /**
* Initialize the Responses module * Initialize the Responses module:
* - Load the system prompt.
* - Configure the OpenAI client.
* - Register the message event handler.
* @param {object} client - Discord client instance.
* @param {object} clientConfig - Full client configuration object.
*/ */
export async function init(client, clientConfig) { export async function init(client, clientConfig) {
const cfg = clientConfig.responses; const cfg = clientConfig.responses;

View File

@ -1,13 +1,18 @@
/**
* Slash command module for '/query'.
* Defines and handles the /query command via the OpenAI Responses API,
* including optional image generation function calls.
*/
import { SlashCommandBuilder, AttachmentBuilder } from 'discord.js'; import { SlashCommandBuilder, AttachmentBuilder } from 'discord.js';
import fs from 'fs/promises'; import fs from 'fs/promises';
import path from 'path'; import path from 'path';
import axios from 'axios'; import axios from 'axios';
/** /**
* Split long text into Discord-safe chunks without breaking mid-line. * Split long text into chunks safe for Discord messaging.
* @param {string} text * @param {string} text - Text to split.
* @param {number} max * @param {number} max - Max characters per chunk.
* @returns {string[]} * @returns {string[]} Array of message chunks.
*/ */
function splitLongMessage(text, max = 2000) { function splitLongMessage(text, max = 2000) {
const lines = text.split('\n'); const lines = text.split('\n');
@ -26,8 +31,14 @@ function splitLongMessage(text, max = 2000) {
} }
/** /**
* Handle 'generate_image' function calls for slash commands. * Handle an OpenAI 'generate_image' function call in a slash-command interaction.
* Returns true if image was sent. * Downloads generated images, saves locally, and replies with attachments.
* @param {object} client - Discord client instance.
* @param {CommandInteraction} interaction - The slash command interaction.
* @param {object} resp - OpenAI Responses API response.
* @param {object} cfg - Module configuration.
* @param {boolean} ephemeral - Whether to reply ephemerally.
* @returns {Promise<boolean>} True if a function call was handled.
*/ */
async function handleImageInteraction(client, interaction, resp, cfg, ephemeral) { async function handleImageInteraction(client, interaction, resp, cfg, ephemeral) {
const calls = Array.isArray(resp.output) ? resp.output : []; const calls = Array.isArray(resp.output) ? resp.output : [];
@ -40,55 +51,90 @@ async function handleImageInteraction(client, interaction, resp, cfg, ephemeral)
await interaction.editReply({ content: 'Cannot generate image: empty prompt.', ephemeral }); await interaction.editReply({ content: 'Cannot generate image: empty prompt.', ephemeral });
return true; return true;
} }
let size; // Always use image model defined in config
switch (args.aspect) { const model = cfg.imageGeneration.defaultModel;
case 'landscape': size = '1792x1024'; break; const promptText = args.prompt;
case 'portrait': size = '1024x1792'; break; // Determine number of images (1-10); DALL·E-3 only supports 1
case 'square': default: size = '1024x1024'; break; let count = 1;
if (args.n != null) {
const nVal = typeof args.n === 'number' ? args.n : parseInt(args.n, 10);
if (!Number.isNaN(nVal)) count = nVal;
} }
const quality = ['standard', 'hd'].includes(args.quality) // clamp
? args.quality count = Math.max(1, Math.min(10, count));
: cfg.imageGeneration.defaultQuality; if (model === 'dall-e-3') count = 1;
const size = args.size || 'auto';
// Determine quality based on config and model constraints
let quality = args.quality || cfg.imageGeneration.defaultQuality;
if (model === 'gpt-image-1') {
if (!['low', 'medium', 'high', 'auto'].includes(quality)) quality = 'auto';
} else if (model === 'dall-e-2') {
quality = 'standard';
} else if (model === 'dall-e-3') {
if (!['standard', 'hd', 'auto'].includes(quality)) quality = 'standard';
}
const background = args.background;
const moderation = args.moderation;
const outputFormat = args.output_format;
const compression = args.output_compression;
const style = args.style;
const user = args.user || interaction.user.id;
try { try {
const imgRes = await client.openai.images.generate({ model: 'dall-e-3', prompt: args.prompt, quality, size, n: 1 }); // Build generate parameters
const url = imgRes.data?.[0]?.url; const genParams = { model, prompt: promptText, n: count, size, quality, user };
if (!url) throw new Error('No image URL'); // response_format supported for DALL·E models (not gpt-image-1)
const dl = await axios.get(url, { responseType: 'arraybuffer' }); if (model !== 'gpt-image-1' && args.response_format) {
const buf = Buffer.from(dl.data); genParams['response_format'] = args.response_format;
const filename = `${interaction.user.id}-${Date.now()}.png`; }
const dir = cfg.imageGeneration.imageSavePath || './images'; // gpt-image-1 supports background, moderation, output_format, and output_compression
if (model === 'gpt-image-1') {
if (background) genParams['background'] = background;
if (moderation) genParams['moderation'] = moderation;
if (outputFormat) {
genParams['output_format'] = outputFormat;
// only support compression for JPEG or WEBP formats
if (['jpeg','webp'].includes(outputFormat) && typeof compression === 'number') {
genParams['output_compression'] = compression;
}
}
}
// dall-e-3 supports style
if (model === 'dall-e-3' && style) {
genParams['style'] = style;
}
// Generate images via OpenAI Images API
const imgRes = await client.openai.images.generate(genParams);
const images = imgRes.data || [];
if (!images.length) throw new Error('No images generated');
// Ensure save directory exists
const dir = cfg.imageGeneration?.imageSavePath || './images';
await fs.mkdir(dir, { recursive: true }); await fs.mkdir(dir, { recursive: true });
const attachments = [];
const outputs = [];
// Process each generated image
for (let i = 0; i < images.length; i++) {
const img = images[i];
let buffer, ext = outputFormat || 'png';
if (img.b64_json) {
buffer = Buffer.from(img.b64_json, 'base64');
outputs.push({ b64_json: img.b64_json });
} else if (img.url) {
const dl = await axios.get(img.url, { responseType: 'arraybuffer' });
buffer = Buffer.from(dl.data);
const parsed = path.extname(img.url.split('?')[0]).replace(/^[.]/, '');
if (parsed) ext = parsed;
outputs.push({ url: img.url });
} else {
throw new Error('No image data');
}
const filename = `${interaction.user.id}-${Date.now()}-${i}.${ext}`;
const filePath = path.join(dir, filename); const filePath = path.join(dir, filename);
await fs.writeFile(filePath, buf); await fs.writeFile(filePath, buffer);
client.logger.info(`Saved image: ${filePath}`); client.logger.info(`Saved image: ${filePath}`);
const attachment = new AttachmentBuilder(buf, { name: filename }); attachments.push(new AttachmentBuilder(buffer, { name: filename }));
await interaction.editReply({ content: args.prompt, files: [attachment] });
// Recap output for context
try {
const convKey = interaction.channelId;
const toolOutputItem = {
type: 'function_call_output',
call_id: fn.call_id,
output: JSON.stringify({ url }),
};
const recapBody = {
model: cfg.defaultModel,
instructions: client.responsesSystemPrompt,
previous_response_id: resp.id,
input: [toolOutputItem],
max_output_tokens: Math.min(100, cfg.defaultMaxTokens),
temperature: cfg.defaultTemperature,
};
const recapResp = await client.openai.responses.create(recapBody);
client.pb?.cache?.set(convKey, recapResp.id, Math.floor(cfg.conversationExpiry / 1000));
const recapTokens = recapResp.usage?.total_tokens ?? recapResp.usage?.completion_tokens ?? 0;
if (client.scorekeeper && recapTokens > 0) {
client.scorekeeper.addOutput(interaction.guildId, interaction.user.id, recapTokens)
.catch(e => client.logger.error(`Scorekeeper error: ${e.message}`));
}
} catch (err) {
client.logger.error(`Recap failed: ${err.message}`);
} }
// Reply with attachments
await interaction.editReply({ content: promptText, files: attachments });
return true; return true;
} catch (err) { } catch (err) {
client.logger.error(`Image generation error: ${err.message}`); client.logger.error(`Image generation error: ${err.message}`);
@ -103,6 +149,9 @@ async function handleImageInteraction(client, interaction, resp, cfg, ephemeral)
* prompt - Required string: the text to send to AI. * prompt - Required string: the text to send to AI.
* ephemeral - Optional boolean: send response ephemerally (default: true). * ephemeral - Optional boolean: send response ephemerally (default: true).
*/ */
/**
* Slash command definitions and handlers for the '/query' command.
*/
export const commands = [ export const commands = [
{ {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
@ -160,19 +209,57 @@ export const commands = [
// Assemble enabled tools // Assemble enabled tools
const tools = []; const tools = [];
if (cfg.tools?.imageGeneration) { if (cfg.tools?.imageGeneration) {
const model = cfg.imageGeneration.defaultModel;
// Configure allowed sizes per model
let sizeEnum;
switch (model) {
case 'gpt-image-1': sizeEnum = ['auto','1024x1024','1536x1024','1024x1536']; break;
case 'dall-e-2': sizeEnum = ['256x256','512x512','1024x1024']; break;
case 'dall-e-3': sizeEnum = ['auto','1024x1024','1792x1024','1024x1792']; break;
default: sizeEnum = ['auto','1024x1024'];
}
// Configure quality options per model
let qualityEnum;
switch (model) {
case 'gpt-image-1': qualityEnum = ['auto','low','medium','high']; break;
case 'dall-e-2': qualityEnum = ['standard']; break;
case 'dall-e-3': qualityEnum = ['auto','standard','hd']; break;
default: qualityEnum = ['auto','standard'];
}
// Build schema properties dynamically
const properties = {
prompt: { type: 'string', description: 'Text description of desired image(s).' },
n: { type: 'number', description: 'Number of images to generate.' },
size: { type: 'string', enum: sizeEnum, description: 'Image size.' },
quality: { type: 'string', enum: qualityEnum, description: 'Image quality.' },
user: { type: 'string', description: 'Unique end-user identifier.' }
};
if (model !== 'gpt-image-1') {
properties.response_format = { type: 'string', enum: ['url','b64_json'], description: 'Format of returned images.' };
}
if (model === 'gpt-image-1') {
properties.background = { type: 'string', enum: ['transparent','opaque','auto'], description: 'Background transparency.' };
properties.moderation = { type: 'string', enum: ['low','auto'], description: 'Content moderation level.' };
properties.output_format = { type: 'string', enum: ['png','jpeg','webp'], description: 'Output image format.' };
properties.output_compression = { type: 'number', description: 'Compression level (0-100).' };
}
if (model === 'dall-e-3') {
properties.style = { type: 'string', enum: ['vivid','natural'], description: 'Style option for dall-e-3.' };
}
// Determine required fields
const required = ['prompt','n','size','quality','user'];
if (model !== 'gpt-image-1') required.push('response_format');
if (model === 'gpt-image-1') required.push('background','moderation','output_format','output_compression');
if (model === 'dall-e-3') required.push('style');
tools.push({ tools.push({
type: 'function', type: 'function',
name: 'generate_image', name: 'generate_image',
description: 'Generate an image with a given prompt, aspect, and quality.', description: `Generate images using model ${model} with requested parameters.`,
parameters: { parameters: {
type: 'object', type: 'object',
properties: { properties,
prompt: { type: 'string' }, required,
aspect: { type: 'string', enum: ['square','portrait','landscape'] }, additionalProperties: false
quality: { type: 'string', enum: ['standard','hd'] },
},
required: ['prompt','aspect','quality'],
additionalProperties: false,
}, },
strict: true, strict: true,
}); });