Skip to content
Merged
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
1 change: 1 addition & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export const globalSettingsSchema = z.object({
dismissedUpsells: z.array(z.string()).optional(),

// Image generation settings (experimental) - flattened for simplicity
imageGenerationProvider: z.enum(["openrouter", "roo"]).optional(),
openRouterImageApiKey: z.string().optional(),
openRouterImageGenerationSelectedModel: z.string().optional(),

Expand Down
31 changes: 27 additions & 4 deletions packages/types/src/image-generation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,39 @@
export interface ImageGenerationModel {
value: string
label: string
provider: ImageGenerationProvider
}

export const IMAGE_GENERATION_MODELS: ImageGenerationModel[] = [
{ value: "google/gemini-2.5-flash-image", label: "Gemini 2.5 Flash Image" },
{ value: "google/gemini-3-pro-image-preview", label: "Gemini 3 Pro Image Preview" },
{ value: "openai/gpt-5-image", label: "GPT-5 Image" },
{ value: "openai/gpt-5-image-mini", label: "GPT-5 Image Mini" },
// OpenRouter models
{ value: "google/gemini-2.5-flash-image", label: "Gemini 2.5 Flash Image", provider: "openrouter" },
{ value: "google/gemini-3-pro-image-preview", label: "Gemini 3 Pro Image Preview", provider: "openrouter" },
{ value: "openai/gpt-5-image", label: "GPT-5 Image", provider: "openrouter" },
{ value: "openai/gpt-5-image-mini", label: "GPT-5 Image Mini", provider: "openrouter" },
// Roo Code Cloud models
{ value: "google/gemini-2.5-flash-image", label: "Gemini 2.5 Flash Image", provider: "roo" },
{ value: "google/gemini-3-pro-image", label: "Gemini 3 Pro Image", provider: "roo" },
]

/**
* Get array of model values only (for backend validation)
*/
export const IMAGE_GENERATION_MODEL_IDS = IMAGE_GENERATION_MODELS.map((m) => m.value)

/**
* Image generation provider type
*/
export type ImageGenerationProvider = "openrouter" | "roo"

/**
* Get the image generation provider with backwards compatibility
* - If provider is explicitly set, use it
* - If a model is already configured (existing users), default to "openrouter"
* - Otherwise default to "roo" (new users)
*/
export function getImageGenerationProvider(
explicitProvider: ImageGenerationProvider | undefined,
hasExistingModel: boolean,
): ImageGenerationProvider {
return explicitProvider !== undefined ? explicitProvider : hasExistingModel ? "openrouter" : "roo"
}
134 changes: 9 additions & 125 deletions src/api/providers/openrouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,33 +26,7 @@ import { DEFAULT_HEADERS } from "./constants"
import { BaseProvider } from "./base-provider"
import type { ApiHandlerCreateMessageMetadata, SingleCompletionHandler } from "../index"
import { handleOpenAIError } from "./utils/openai-error-handler"

// Image generation types
interface ImageGenerationResponse {
choices?: Array<{
message?: {
content?: string
images?: Array<{
type?: string
image_url?: {
url?: string
}
}>
}
}>
error?: {
message?: string
type?: string
code?: string
}
}

export interface ImageGenerationResult {
success: boolean
imageData?: string
imageFormat?: string
error?: string
}
import { generateImageWithProvider, ImageGenerationResult } from "./utils/image-generation"

// Add custom interface for OpenRouter params.
type OpenRouterChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & {
Expand Down Expand Up @@ -387,103 +361,13 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
}
}

try {
const baseURL = this.options.openRouterBaseUrl || "https://openrouter.ai/api/v1"
const response = await fetch(`${baseURL}/chat/completions`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"HTTP-Referer": "https://github.com/RooVetGit/Roo-Code",
"X-Title": "Roo Code",
},
body: JSON.stringify({
model,
messages: [
{
role: "user",
content: inputImage
? [
{
type: "text",
text: prompt,
},
{
type: "image_url",
image_url: {
url: inputImage,
},
},
]
: prompt,
},
],
modalities: ["image", "text"],
}),
})

if (!response.ok) {
const errorText = await response.text()
let errorMessage = `Failed to generate image: ${response.status} ${response.statusText}`
try {
const errorJson = JSON.parse(errorText)
if (errorJson.error?.message) {
errorMessage = `Failed to generate image: ${errorJson.error.message}`
}
} catch {
// Use default error message
}
return {
success: false,
error: errorMessage,
}
}

const result: ImageGenerationResponse = await response.json()

if (result.error) {
return {
success: false,
error: `Failed to generate image: ${result.error.message}`,
}
}

// Extract the generated image from the response
const images = result.choices?.[0]?.message?.images
if (!images || images.length === 0) {
return {
success: false,
error: "No image was generated in the response",
}
}

const imageData = images[0]?.image_url?.url
if (!imageData) {
return {
success: false,
error: "Invalid image data in response",
}
}

// Extract base64 data from data URL
const base64Match = imageData.match(/^data:image\/(png|jpeg|jpg);base64,(.+)$/)
if (!base64Match) {
return {
success: false,
error: "Invalid image format received",
}
}

return {
success: true,
imageData: imageData,
imageFormat: base64Match[1],
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error occurred",
}
}
const baseURL = this.options.openRouterBaseUrl || "https://openrouter.ai/api/v1"
return generateImageWithProvider({
baseURL,
authToken: apiKey,
model,
prompt,
inputImage,
})
}
}
28 changes: 28 additions & 0 deletions src/api/providers/roo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import type { ApiHandlerCreateMessageMetadata } from "../index"
import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider"
import { getModels, getModelsFromCache } from "../providers/fetchers/modelCache"
import { handleOpenAIError } from "./utils/openai-error-handler"
import { generateImageWithProvider, ImageGenerationResult } from "./utils/image-generation"
import { t } from "../../i18n"

// Extend OpenAI's CompletionUsage to include Roo specific fields
interface RooUsage extends OpenAI.CompletionUsage {
Expand Down Expand Up @@ -305,4 +307,30 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
info: fallbackInfo,
}
}

/**
* Generate an image using Roo Code Cloud's image generation API
* @param prompt The text prompt for image generation
* @param model The model to use for generation
* @param inputImage Optional base64 encoded input image data URL
* @returns The generated image data and format, or an error
*/
async generateImage(prompt: string, model: string, inputImage?: string): Promise<ImageGenerationResult> {
const sessionToken = getSessionToken()

if (!sessionToken || sessionToken === "unauthenticated") {
return {
success: false,
error: t("tools:generateImage.roo.authRequired"),
}
}

return generateImageWithProvider({
baseURL: `${this.fetcherBaseURL}/v1`,
authToken: sessionToken,
model,
prompt,
inputImage,
})
}
}
149 changes: 149 additions & 0 deletions src/api/providers/utils/image-generation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { t } from "../../../i18n"

// Image generation types
interface ImageGenerationResponse {
choices?: Array<{
message?: {
content?: string
images?: Array<{
type?: string
image_url?: {
url?: string
}
}>
}
}>
error?: {
message?: string
type?: string
code?: string
}
}

export interface ImageGenerationResult {
success: boolean
imageData?: string
imageFormat?: string
error?: string
}

interface ImageGenerationOptions {
baseURL: string
authToken: string
model: string
prompt: string
inputImage?: string
}

/**
* Shared image generation implementation for OpenRouter and Roo Code Cloud providers
*/
export async function generateImageWithProvider(options: ImageGenerationOptions): Promise<ImageGenerationResult> {
const { baseURL, authToken, model, prompt, inputImage } = options

try {
const response = await fetch(`${baseURL}/chat/completions`, {
method: "POST",
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
"HTTP-Referer": "https://github.com/RooVetGit/Roo-Code",
"X-Title": "Roo Code",
},
body: JSON.stringify({
model,
messages: [
{
role: "user",
content: inputImage
? [
{
type: "text",
text: prompt,
},
{
type: "image_url",
image_url: {
url: inputImage,
},
},
]
: prompt,
},
],
modalities: ["image", "text"],
}),
})

if (!response.ok) {
const errorText = await response.text()
let errorMessage = t("tools:generateImage.failedWithStatus", {
status: response.status,
statusText: response.statusText,
})

try {
const errorJson = JSON.parse(errorText)
if (errorJson.error?.message) {
errorMessage = t("tools:generateImage.failedWithMessage", {
message: errorJson.error.message,
})
}
} catch {
// Use default error message
}
return {
success: false,
error: errorMessage,
}
}

const result: ImageGenerationResponse = await response.json()

if (result.error) {
return {
success: false,
error: t("tools:generateImage.failedWithMessage", {
message: result.error.message,
}),
}
}

// Extract the generated image from the response
const images = result.choices?.[0]?.message?.images
if (!images || images.length === 0) {
return {
success: false,
error: t("tools:generateImage.noImageGenerated"),
}
}

const imageData = images[0]?.image_url?.url
if (!imageData) {
return {
success: false,
error: t("tools:generateImage.invalidImageData"),
}
}

// Extract base64 data from data URL
const base64Match = imageData.match(/^data:image\/(png|jpeg|jpg);base64,(.+)$/)
if (!base64Match) {
return {
success: false,
error: t("tools:generateImage.invalidImageFormat"),
}
}

return {
success: true,
imageData: imageData,
imageFormat: base64Match[1],
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : t("tools:generateImage.unknownError"),
}
}
}
Loading
Loading