Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions plugins/discourse-ai/app/models/ai_tool.rb
Original file line number Diff line number Diff line change
Expand Up @@ -240,9 +240,9 @@ def self.image_generation_presets
},
{
preset_id: "image_generation_flux",
name: "FLUX",
name: "FLUX 1.1 Pro",
provider: "Together.ai",
model_name: "FLUX 1.1",
model_name: "FLUX 1.1 Pro",
tool_name: "image_generation",
description:
"Generate images using the FLUX 1.1 Pro model from Black Forest Labs via Together.ai",
Expand All @@ -260,10 +260,36 @@ def self.image_generation_presets
description: "Optional seed for random number generation",
},
],
script: "#{preamble}\n#{load_script("presets/image_generation/flux.js")}",
script: "#{preamble}\n#{load_script("presets/image_generation/flux_together.js")}",
summary: "Generate images with FLUX 1.1 Pro",
category: "image_generation",
},
{
preset_id: "image_generation_flux2",
name: "FLUX 2 Pro",
provider: "Black Forest Labs",
model_name: "FLUX 2 Pro",
tool_name: "image_generation_flux2",
description:
"Generate and edit images using FLUX 2 Pro directly via Black Forest Labs API. Supports multi-image editing.",
parameters: [
{
name: "prompt",
type: "string",
required: true,
description: "The text prompt for image generation or editing",
},
{
name: "seed",
type: "number",
required: false,
description: "Optional seed for reproducible results",
},
],
script: "#{preamble}\n#{load_script("presets/image_generation/flux_2_bfl.js")}",
summary: "Generate and edit images with FLUX 2 Pro",
category: "image_generation",
},
]
end
end
Expand Down
4 changes: 3 additions & 1 deletion plugins/discourse-ai/config/locales/server.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,9 @@ en:
image_generation_gemini:
name: "Nano Banana"
image_generation_flux:
name: "FLUX"
name: "FLUX 1.1 Pro"
image_generation_flux2:
name: "FLUX 2 Pro"
empty_tool:
name: "Start from blank..."
name:
Expand Down
52 changes: 24 additions & 28 deletions plugins/discourse-ai/config/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,31 +34,12 @@ discourse_ai:
default: 60
hidden: true

ai_openai_image_generation_url: "https://api.openai.com/v1/images/generations"
ai_openai_image_edit_url: "https://api.openai.com/v1/images/edits"
ai_openai_embeddings_url:
hidden: true
default: "https://api.openai.com/v1/embeddings"
ai_openai_organization:
default: ""
hidden: true
ai_openai_api_key:
default: ""
secret: true
ai_stability_api_key:
default: ""
secret: true
ai_stability_api_url:
default: "https://api.stability.ai"
ai_stability_engine:
default: "stable-diffusion-xl-1024-v1-0"
type: enum
choices:
- "sd3"
- "sd3-turbo"
- "stable-diffusion-xl-1024-v1-0"
- "stable-diffusion-768-v2-1"
- "stable-diffusion-v1-5"
ai_hugging_face_tei_endpoint:
hidden: true
default: ""
Expand Down Expand Up @@ -96,6 +77,30 @@ discourse_ai:
default: false
hidden: true

# Deprecated image generation settings - TODO(keegan): Remove 2026-02-01
ai_openai_image_generation_url:
default: ""
hidden: true
ai_openai_image_edit_url:
default: ""
hidden: true
ai_stability_api_key:
default: ""
secret: true
hidden: true
ai_stability_api_url:
default: ""
hidden: true
ai_stability_engine:
default: ""
hidden: true
ai_helper_illustrate_post_model:
default: ""
type: enum
allow_any: false
enum: "DiscourseAi::Configuration::LlmEnumerator"
hidden: true

ai_helper_enabled:
default: false
client: true
Expand Down Expand Up @@ -138,15 +143,6 @@ discourse_ai:
ai_helper_automatic_chat_thread_title_delay:
default: 5
area: "ai-features/ai_helper"
ai_helper_illustrate_post_model: # Deprecated. TODO(keegan): Remove 2025-12-01
default: disabled
type: enum
choices:
- stable_diffusion_xl
- dall_e_3
- disabled
area: "ai-features/ai_helper"
hidden: true
ai_helper_enabled_features:
client: true
default: "suggestions|context_menu"
Expand Down
15 changes: 15 additions & 0 deletions plugins/discourse-ai/lib/ai_tool_scripts/preamble.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,21 @@
* Returns: { id: number, url: string, short_url: string } - Details of the created upload record.
*
* upload.getUrl(shortUrl): Given a short URL, eg upload://12345, returns the full CDN friendly URL of the upload.
*
* upload.getBase64(uploadIdOrShortUrl, maxPixels): Fetches the base64-encoded content of an existing upload.
* Parameters:
* uploadIdOrShortUrl (number | string): Either an upload ID (number) or short URL (string, eg "upload://abc123").
* maxPixels (number, optional): Maximum pixel count for automatic resizing (default: 10,000,000).
* Returns: string (base64-encoded image data) or null if upload not found.
* Use case: Image editing tools can fetch existing uploads to send to external APIs.
*
* Note for Image Editing:
* To implement image editing in a tool:
* 1. Accept an `image_urls` parameter (array of short URLs like ["upload://abc123"]).
* 2. Use upload.getBase64() to fetch the base64 data for each image.
* 3. Send the base64 data to your image editing API (e.g., OpenAI's /v1/images/edits endpoint).
* 4. Create a new upload with the edited image using upload.create().
* 5. Use chain.setCustomRaw() to display the edited image.
* 5. chain
* Controls the execution flow.
*
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/* eslint-disable no-undef, no-unused-vars */
const apiKey = "YOUR_BFL_API_KEY";

function invoke(params) {
const prompt = params.prompt;
const imageUrls = params.image_urls || [];

let seed = parseInt(params.seed, 10);
if (!(seed > 0)) {
seed = Math.floor(Math.random() * 2147483647) + 1;
}

if (imageUrls.length > 0) {
return performEdit(prompt, imageUrls, seed);
} else {
return performGeneration(prompt, seed);
}
}

function performGeneration(prompt, seed) {
const body = {
prompt,
seed,
width: 1024,
height: 1024,
output_format: "png",
safety_tolerance: 2,
};

return submitAndPoll(body, prompt, seed);
}

function performEdit(prompt, imageUrls, seed) {
const body = {
prompt,
seed,
output_format: "png",
safety_tolerance: 2,
};

// Add up to 10 reference images as base64
const maxImages = Math.min(imageUrls.length, 10);
for (let i = 0; i < maxImages; i++) {
const base64Data = upload.getBase64(imageUrls[i]);
if (!base64Data) {
return { error: `Failed to get base64 data for: ${imageUrls[i]}` };
}
const paramName = i === 0 ? "input_image" : `input_image_${i + 1}`;
body[paramName] = base64Data;
}

return submitAndPoll(body, prompt, seed);
}

function submitAndPoll(body, prompt, seed) {
// Submit request
const submitResult = http.post("https://api.bfl.ai/v1/flux-2-pro", {
headers: {
"x-key": apiKey,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});

const submitData = JSON.parse(submitResult.body);

if (submitData.error) {
return { error: `BFL API Error: ${submitData.error}` };
}

if (!submitData.id) {
return {
error: "No task ID returned",
body_preview: JSON.stringify(submitData).substring(0, 500),
};
}

// Poll for result (max 25 attempts × 3s = 75s)
const pollingUrl = `https://api.bfl.ai/v1/get_result?id=${submitData.id}`;

for (let attempt = 0; attempt < 25; attempt++) {
const pollResult = http.get(pollingUrl, {
headers: { "x-key": apiKey },
});

const pollData = JSON.parse(pollResult.body);

if (pollData.status === "Ready") {
// Download image from signed URL
const imageUrl = pollData.result.sample;
const imageResponse = http.get(imageUrl, { base64Encode: true });

if (!imageResponse.body) {
return { error: "Failed to download generated image" };
}

const image = upload.create("generated_image.png", imageResponse.body);

if (!image || image.error) {
return {
error: `Upload failed: ${image ? image.error : "unknown"}`,
};
}

const raw = `\n![${prompt}](${image.short_url})\n`;
chain.setCustomRaw(raw);

return { result: "Image generated successfully", seed };
}

if (
pollData.status === "Failed" ||
pollData.status === "Error" ||
pollData.status === "Request Moderated"
) {
return {
error: `Generation failed: ${pollData.error || pollData.status}`,
};
}

// Wait 3 seconds before next poll
sleep(3000);
}

return { error: "Generation timed out after 75 seconds" };
}

function details() {
return "Generates and edits images using FLUX 2 Pro via Black Forest Labs API. Supports multi-image editing with up to 10 reference images.";
}
Loading