diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index f1e31404f28..6e61c3950f5 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -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(), diff --git a/packages/types/src/image-generation.ts b/packages/types/src/image-generation.ts index 2acd281031d..2317a11b802 100644 --- a/packages/types/src/image-generation.ts +++ b/packages/types/src/image-generation.ts @@ -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" +} diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index c63142aad92..a5501650403 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -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 & { @@ -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, + }) } } diff --git a/src/api/providers/roo.ts b/src/api/providers/roo.ts index 393740d3bd4..a5ddd4dc4a9 100644 --- a/src/api/providers/roo.ts +++ b/src/api/providers/roo.ts @@ -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 { @@ -305,4 +307,30 @@ export class RooHandler extends BaseOpenAiCompatibleProvider { 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 { + 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, + }) + } } diff --git a/src/api/providers/utils/image-generation.ts b/src/api/providers/utils/image-generation.ts new file mode 100644 index 00000000000..113dc3383e0 --- /dev/null +++ b/src/api/providers/utils/image-generation.ts @@ -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 { + 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"), + } + } +} diff --git a/src/core/tools/GenerateImageTool.ts b/src/core/tools/GenerateImageTool.ts index 18e88827548..4c4f6819155 100644 --- a/src/core/tools/GenerateImageTool.ts +++ b/src/core/tools/GenerateImageTool.ts @@ -1,7 +1,12 @@ import path from "path" import fs from "fs/promises" import * as vscode from "vscode" -import { GenerateImageParams, IMAGE_GENERATION_MODEL_IDS } from "@roo-code/types" +import { + GenerateImageParams, + IMAGE_GENERATION_MODEL_IDS, + IMAGE_GENERATION_MODELS, + getImageGenerationProvider, +} from "@roo-code/types" import { Task } from "../task/Task" import { formatResponse } from "../prompts/responses" import { fileExistsAtPath } from "../../utils/fs" @@ -9,8 +14,10 @@ import { getReadablePath } from "../../utils/path" import { isPathOutsideWorkspace } from "../../utils/pathUtils" import { EXPERIMENT_IDS, experiments } from "../../shared/experiments" import { OpenRouterHandler } from "../../api/providers/openrouter" +import { RooHandler } from "../../api/providers/roo" import { BaseTool, ToolCallbacks } from "./BaseTool" import type { ToolUse } from "../../shared/tools" +import { t } from "../../i18n" export class GenerateImageTool extends BaseTool<"generate_image"> { readonly name = "generate_image" as const @@ -120,23 +127,43 @@ export class GenerateImageTool extends BaseTool<"generate_image"> { const isWriteProtected = task.rooProtectedController?.isWriteProtected(relPath) || false + // Use shared utility for backwards compatibility logic + const imageProvider = getImageGenerationProvider( + state?.imageGenerationProvider, + !!state?.openRouterImageGenerationSelectedModel, + ) + + // Get the selected model + let selectedModel = state?.openRouterImageGenerationSelectedModel + + // Verify the selected model matches the selected provider + // If not, default to first model of the selected provider + if (selectedModel) { + const modelInfo = IMAGE_GENERATION_MODELS.find((m) => m.value === selectedModel) + if (!modelInfo || modelInfo.provider !== imageProvider) { + // Model doesn't match provider, use first model for selected provider + const providerModels = IMAGE_GENERATION_MODELS.filter((m) => m.provider === imageProvider) + selectedModel = providerModels[0]?.value || IMAGE_GENERATION_MODEL_IDS[0] + } + } else { + // No model selected, use first model for selected provider + const providerModels = IMAGE_GENERATION_MODELS.filter((m) => m.provider === imageProvider) + selectedModel = providerModels[0]?.value || IMAGE_GENERATION_MODEL_IDS[0] + } + + // Use the provider selection + const modelProvider = imageProvider + + // Validate API key for OpenRouter const openRouterApiKey = state?.openRouterImageApiKey - if (!openRouterApiKey) { - await task.say( - "error", - "OpenRouter API key is required for image generation. Please configure it in the Image Generation experimental settings.", - ) - pushToolResult( - formatResponse.toolError( - "OpenRouter API key is required for image generation. Please configure it in the Image Generation experimental settings.", - ), - ) + if (imageProvider === "openrouter" && !openRouterApiKey) { + const errorMessage = t("tools:generateImage.openRouterApiKeyRequired") + await task.say("error", errorMessage) + pushToolResult(formatResponse.toolError(errorMessage)) return } - const selectedModel = state?.openRouterImageGenerationSelectedModel || IMAGE_GENERATION_MODEL_IDS[0] - const fullPath = path.resolve(task.cwd, removeClosingTag("path", relPath)) const isOutsideWorkspace = isPathOutsideWorkspace(fullPath) @@ -163,14 +190,16 @@ export class GenerateImageTool extends BaseTool<"generate_image"> { return } - const openRouterHandler = new OpenRouterHandler({} as any) - - const result = await openRouterHandler.generateImage( - prompt, - selectedModel, - openRouterApiKey, - inputImageData, - ) + let result + if (modelProvider === "roo") { + // Use Roo Code Cloud provider + const rooHandler = new RooHandler({} as any) + result = await rooHandler.generateImage(prompt, selectedModel, inputImageData) + } else { + // Use OpenRouter provider + const openRouterHandler = new OpenRouterHandler({} as any) + result = await openRouterHandler.generateImage(prompt, selectedModel, openRouterApiKey!, inputImageData) + } if (!result.success) { await task.say("error", result.error || "Failed to generate image") diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 240844c08d8..a16de9b88a5 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1921,6 +1921,7 @@ export class ClineProvider maxGitStatusFiles, taskSyncEnabled, remoteControlEnabled, + imageGenerationProvider, openRouterImageApiKey, openRouterImageGenerationSelectedModel, openRouterUseMiddleOutTransform, @@ -2088,6 +2089,7 @@ export class ClineProvider maxGitStatusFiles: maxGitStatusFiles ?? 0, taskSyncEnabled, remoteControlEnabled, + imageGenerationProvider, openRouterImageApiKey, openRouterImageGenerationSelectedModel, openRouterUseMiddleOutTransform, @@ -2318,6 +2320,7 @@ export class ClineProvider return false } })(), + imageGenerationProvider: stateValues.imageGenerationProvider, openRouterImageApiKey: stateValues.openRouterImageApiKey, openRouterImageGenerationSelectedModel: stateValues.openRouterImageGenerationSelectedModel, featureRoomoteControlEnabled: (() => { diff --git a/src/i18n/locales/ca/tools.json b/src/i18n/locales/ca/tools.json index 0f10b6fc2a1..f63bdea76e5 100644 --- a/src/i18n/locales/ca/tools.json +++ b/src/i18n/locales/ca/tools.json @@ -14,5 +14,17 @@ "errors": { "policy_restriction": "No s'ha pogut crear una nova tasca a causa de restriccions de política." } + }, + "generateImage": { + "openRouterApiKeyRequired": "Es requereix la clau API d'OpenRouter per a la generació d'imatges amb models d'OpenRouter. Configura-la a la configuració experimental de Generació d'Imatges.", + "failedWithStatus": "Error en generar imatge: {{status}} {{statusText}}", + "failedWithMessage": "Error en generar imatge: {{message}}", + "noImageGenerated": "No s'ha generat cap imatge en la resposta", + "invalidImageData": "Dades d'imatge no vàlides en la resposta", + "invalidImageFormat": "Format d'imatge no vàlid rebut", + "unknownError": "S'ha produït un error desconegut", + "roo": { + "authRequired": "Es requereix autenticació de Roo Code Cloud per a la generació d'imatges. Inicia sessió a Roo Code Cloud." + } } } diff --git a/src/i18n/locales/de/tools.json b/src/i18n/locales/de/tools.json index ecf372a50bf..0effb1011a5 100644 --- a/src/i18n/locales/de/tools.json +++ b/src/i18n/locales/de/tools.json @@ -14,5 +14,17 @@ "errors": { "policy_restriction": "Neue Aufgabe konnte aufgrund von Richtlinienbeschränkungen nicht erstellt werden." } + }, + "generateImage": { + "openRouterApiKeyRequired": "OpenRouter API-Schlüssel ist für die Bildgenerierung mit OpenRouter-Modellen erforderlich. Bitte konfiguriere ihn in den Experimentellen Einstellungen für Bildgenerierung.", + "failedWithStatus": "Bildgenerierung fehlgeschlagen: {{status}} {{statusText}}", + "failedWithMessage": "Bildgenerierung fehlgeschlagen: {{message}}", + "noImageGenerated": "In der Antwort wurde kein Bild generiert", + "invalidImageData": "Ungültige Bilddaten in der Antwort", + "invalidImageFormat": "Ungültiges Bildformat erhalten", + "unknownError": "Unbekannter Fehler aufgetreten", + "roo": { + "authRequired": "Roo Code Cloud-Authentifizierung ist für die Bildgenerierung erforderlich. Bitte melde dich bei Roo Code Cloud an." + } } } diff --git a/src/i18n/locales/en/tools.json b/src/i18n/locales/en/tools.json index 5b88affae6f..48dcbc24f12 100644 --- a/src/i18n/locales/en/tools.json +++ b/src/i18n/locales/en/tools.json @@ -14,5 +14,17 @@ "errors": { "policy_restriction": "Failed to create new task due to policy restrictions." } + }, + "generateImage": { + "openRouterApiKeyRequired": "OpenRouter API key is required for image generation with OpenRouter models. Please configure it in the Image Generation experimental settings.", + "failedWithStatus": "Failed to generate image: {{status}} {{statusText}}", + "failedWithMessage": "Failed to generate image: {{message}}", + "noImageGenerated": "No image was generated in the response", + "invalidImageData": "Invalid image data in response", + "invalidImageFormat": "Invalid image format received", + "unknownError": "Unknown error occurred", + "roo": { + "authRequired": "Roo Code Cloud authentication is required for image generation. Please sign in to Roo Code Cloud." + } } } diff --git a/src/i18n/locales/es/tools.json b/src/i18n/locales/es/tools.json index 6fd1cc21222..9bd5d25c7d0 100644 --- a/src/i18n/locales/es/tools.json +++ b/src/i18n/locales/es/tools.json @@ -14,5 +14,17 @@ "errors": { "policy_restriction": "No se pudo crear una nueva tarea debido a restricciones de política." } + }, + "generateImage": { + "openRouterApiKeyRequired": "Se requiere la clave API de OpenRouter para la generación de imágenes con modelos de OpenRouter. Por favor, configúrala en la configuración experimental de Generación de Imágenes.", + "failedWithStatus": "Error al generar imagen: {{status}} {{statusText}}", + "failedWithMessage": "Error al generar imagen: {{message}}", + "noImageGenerated": "No se generó ninguna imagen en la respuesta", + "invalidImageData": "Datos de imagen no válidos en la respuesta", + "invalidImageFormat": "Formato de imagen no válido recibido", + "unknownError": "Ocurrió un error desconocido", + "roo": { + "authRequired": "Se requiere autenticación de Roo Code Cloud para la generación de imágenes. Por favor, inicia sesión en Roo Code Cloud." + } } } diff --git a/src/i18n/locales/fr/tools.json b/src/i18n/locales/fr/tools.json index b6d7accebb9..550cdbeb87c 100644 --- a/src/i18n/locales/fr/tools.json +++ b/src/i18n/locales/fr/tools.json @@ -14,5 +14,17 @@ "errors": { "policy_restriction": "Impossible de créer une nouvelle tâche en raison de restrictions de politique." } + }, + "generateImage": { + "openRouterApiKeyRequired": "La clé API OpenRouter est requise pour la génération d'images avec les modèles OpenRouter. Veuillez la configurer dans les paramètres expérimentaux de Génération d'Images.", + "failedWithStatus": "Échec de génération de l'image : {{status}} {{statusText}}", + "failedWithMessage": "Échec de génération de l'image : {{message}}", + "noImageGenerated": "Aucune image n'a été générée dans la réponse", + "invalidImageData": "Données d'image non valides dans la réponse", + "invalidImageFormat": "Format d'image non valide reçu", + "unknownError": "Une erreur inconnue s'est produite", + "roo": { + "authRequired": "L'authentification Roo Code Cloud est requise pour la génération d'images. Veuillez vous connecter à Roo Code Cloud." + } } } diff --git a/src/i18n/locales/hi/tools.json b/src/i18n/locales/hi/tools.json index cbfbd7aef70..34ffea448be 100644 --- a/src/i18n/locales/hi/tools.json +++ b/src/i18n/locales/hi/tools.json @@ -14,5 +14,17 @@ "errors": { "policy_restriction": "नीति प्रतिबंधों के कारण नया कार्य बनाने में विफल।" } + }, + "generateImage": { + "openRouterApiKeyRequired": "OpenRouter मॉडल के साथ छवि निर्माण के लिए OpenRouter API कुंजी आवश्यक है। कृपया इसे छवि निर्माण प्रयोगात्मक सेटिंग में कॉन्फ़िगर करें।", + "failedWithStatus": "छवि निर्माण विफल: {{status}} {{statusText}}", + "failedWithMessage": "छवि निर्माण विफल: {{message}}", + "noImageGenerated": "प्रतिक्रिया में कोई छवि उत्पन्न नहीं हुई", + "invalidImageData": "प्रतिक्रिया में अमान्य छवि डेटा", + "invalidImageFormat": "अमान्य छवि प्रारूप प्राप्त हुआ", + "unknownError": "एक अज्ञात त्रुटि हुई", + "roo": { + "authRequired": "छवि निर्माण के लिए Roo Code Cloud प्रमाणीकरण आवश्यक है। कृपया Roo Code Cloud में साइन इन करें।" + } } } diff --git a/src/i18n/locales/id/tools.json b/src/i18n/locales/id/tools.json index 3eb8854eff0..bf3f076bd27 100644 --- a/src/i18n/locales/id/tools.json +++ b/src/i18n/locales/id/tools.json @@ -17,5 +17,17 @@ "errors": { "policy_restriction": "Gagal membuat tugas baru karena pembatasan kebijakan." } + }, + "generateImage": { + "openRouterApiKeyRequired": "Kunci API OpenRouter diperlukan untuk menghasilkan gambar dengan model OpenRouter. Silakan konfigurasikan di pengaturan eksperimental Pembuatan Gambar.", + "failedWithStatus": "Gagal menghasilkan gambar: {{status}} {{statusText}}", + "failedWithMessage": "Gagal menghasilkan gambar: {{message}}", + "noImageGenerated": "Tidak ada gambar yang dihasilkan dalam respons", + "invalidImageData": "Data gambar tidak valid dalam respons", + "invalidImageFormat": "Format gambar tidak valid diterima", + "unknownError": "Terjadi kesalahan yang tidak diketahui", + "roo": { + "authRequired": "Autentikasi Roo Code Cloud diperlukan untuk menghasilkan gambar. Silakan masuk ke Roo Code Cloud." + } } } diff --git a/src/i18n/locales/it/tools.json b/src/i18n/locales/it/tools.json index 35b114a7198..72439f0d29f 100644 --- a/src/i18n/locales/it/tools.json +++ b/src/i18n/locales/it/tools.json @@ -14,5 +14,17 @@ "errors": { "policy_restriction": "Impossibile creare una nuova attività a causa di restrizioni di policy." } + }, + "generateImage": { + "openRouterApiKeyRequired": "La chiave API OpenRouter è richiesta per la generazione di immagini con i modelli OpenRouter. Configurala nelle impostazioni sperimentali di Generazione Immagini.", + "failedWithStatus": "Generazione immagine fallita: {{status}} {{statusText}}", + "failedWithMessage": "Generazione immagine fallita: {{message}}", + "noImageGenerated": "Nessuna immagine è stata generata nella risposta", + "invalidImageData": "Dati immagine non validi nella risposta", + "invalidImageFormat": "Formato immagine non valido ricevuto", + "unknownError": "Si è verificato un errore sconosciuto", + "roo": { + "authRequired": "L'autenticazione Roo Code Cloud è richiesta per la generazione di immagini. Accedi a Roo Code Cloud." + } } } diff --git a/src/i18n/locales/ja/tools.json b/src/i18n/locales/ja/tools.json index 257d5aa2013..66f04012a3e 100644 --- a/src/i18n/locales/ja/tools.json +++ b/src/i18n/locales/ja/tools.json @@ -14,5 +14,17 @@ "errors": { "policy_restriction": "ポリシー制限により新しいタスクを作成できませんでした。" } + }, + "generateImage": { + "openRouterApiKeyRequired": "OpenRouterモデルでの画像生成にはOpenRouter APIキーが必要です。画像生成の実験設定で設定してください。", + "failedWithStatus": "画像生成に失敗しました: {{status}} {{statusText}}", + "failedWithMessage": "画像生成に失敗しました: {{message}}", + "noImageGenerated": "レスポンスに画像が生成されませんでした", + "invalidImageData": "レスポンスに無効な画像データがあります", + "invalidImageFormat": "無効な画像フォーマットを受信しました", + "unknownError": "不明なエラーが発生しました", + "roo": { + "authRequired": "画像生成にはRoo Code Cloud認証が必要です。Roo Code Cloudにサインインしてください。" + } } } diff --git a/src/i18n/locales/ko/tools.json b/src/i18n/locales/ko/tools.json index 94b6d8c3770..f17cafb62da 100644 --- a/src/i18n/locales/ko/tools.json +++ b/src/i18n/locales/ko/tools.json @@ -14,5 +14,17 @@ "errors": { "policy_restriction": "정책 제한으로 인해 새 작업을 생성하지 못했습니다." } + }, + "generateImage": { + "openRouterApiKeyRequired": "OpenRouter 모델로 이미지를 생성하려면 OpenRouter API 키가 필요합니다. 이미지 생성 실험 설정에서 구성하세요.", + "failedWithStatus": "이미지 생성 실패: {{status}} {{statusText}}", + "failedWithMessage": "이미지 생성 실패: {{message}}", + "noImageGenerated": "응답에서 이미지가 생성되지 않았습니다", + "invalidImageData": "응답에 잘못된 이미지 데이터가 있습니다", + "invalidImageFormat": "잘못된 이미지 형식을 받았습니다", + "unknownError": "알 수 없는 오류가 발생했습니다", + "roo": { + "authRequired": "이미지 생성에는 Roo Code Cloud 인증이 필요합니다. Roo Code Cloud에 로그인하세요." + } } } diff --git a/src/i18n/locales/nl/tools.json b/src/i18n/locales/nl/tools.json index 449cd545837..48d9a8c4c9f 100644 --- a/src/i18n/locales/nl/tools.json +++ b/src/i18n/locales/nl/tools.json @@ -14,5 +14,17 @@ "errors": { "policy_restriction": "Kan geen nieuwe taak aanmaken vanwege beleidsbeperkingen." } + }, + "generateImage": { + "openRouterApiKeyRequired": "OpenRouter API-sleutel is vereist voor het genereren van afbeeldingen met OpenRouter-modellen. Configureer deze in de Experimentele Instellingen voor Afbeeldingsgeneratie.", + "failedWithStatus": "Afbeelding genereren mislukt: {{status}} {{statusText}}", + "failedWithMessage": "Afbeelding genereren mislukt: {{message}}", + "noImageGenerated": "Er is geen afbeelding gegenereerd in het antwoord", + "invalidImageData": "Ongeldige afbeeldingsgegevens in het antwoord", + "invalidImageFormat": "Ongeldig afbeeldingsformaat ontvangen", + "unknownError": "Er is een onbekende fout opgetreden", + "roo": { + "authRequired": "Roo Code Cloud-authenticatie is vereist voor het genereren van afbeeldingen. Log in bij Roo Code Cloud." + } } } diff --git a/src/i18n/locales/pl/tools.json b/src/i18n/locales/pl/tools.json index 979b2f54ae0..6969bc5a524 100644 --- a/src/i18n/locales/pl/tools.json +++ b/src/i18n/locales/pl/tools.json @@ -14,5 +14,17 @@ "errors": { "policy_restriction": "Nie udało się utworzyć nowego zadania z powodu ograniczeń polityki." } + }, + "generateImage": { + "openRouterApiKeyRequired": "Klucz API OpenRouter jest wymagany do generowania obrazów z modelami OpenRouter. Skonfiguruj go w eksperymentalnych ustawieniach generowania obrazów.", + "failedWithStatus": "Nie udało się wygenerować obrazu: {{status}} {{statusText}}", + "failedWithMessage": "Nie udało się wygenerować obrazu: {{message}}", + "noImageGenerated": "W odpowiedzi nie wygenerowano żadnego obrazu", + "invalidImageData": "Nieprawidłowe dane obrazu w odpowiedzi", + "invalidImageFormat": "Otrzymano nieprawidłowy format obrazu", + "unknownError": "Wystąpił nieznany błąd", + "roo": { + "authRequired": "Uwierzytelnienie Roo Code Cloud jest wymagane do generowania obrazów. Zaloguj się do Roo Code Cloud." + } } } diff --git a/src/i18n/locales/pt-BR/tools.json b/src/i18n/locales/pt-BR/tools.json index 4e3296fd4a6..e19be77fa58 100644 --- a/src/i18n/locales/pt-BR/tools.json +++ b/src/i18n/locales/pt-BR/tools.json @@ -14,5 +14,17 @@ "errors": { "policy_restriction": "Falha ao criar nova tarefa devido a restrições de política." } + }, + "generateImage": { + "openRouterApiKeyRequired": "A chave API do OpenRouter é necessária para geração de imagens com modelos OpenRouter. Configure-a nas configurações experimentais de Geração de Imagens.", + "failedWithStatus": "Falha ao gerar imagem: {{status}} {{statusText}}", + "failedWithMessage": "Falha ao gerar imagem: {{message}}", + "noImageGenerated": "Nenhuma imagem foi gerada na resposta", + "invalidImageData": "Dados de imagem inválidos na resposta", + "invalidImageFormat": "Formato de imagem inválido recebido", + "unknownError": "Ocorreu um erro desconhecido", + "roo": { + "authRequired": "A autenticação do Roo Code Cloud é necessária para geração de imagens. Faça login no Roo Code Cloud." + } } } diff --git a/src/i18n/locales/ru/tools.json b/src/i18n/locales/ru/tools.json index d74918f058e..c25b3b26bfd 100644 --- a/src/i18n/locales/ru/tools.json +++ b/src/i18n/locales/ru/tools.json @@ -14,5 +14,17 @@ "errors": { "policy_restriction": "Не удалось создать новую задачу из-за ограничений политики." } + }, + "generateImage": { + "openRouterApiKeyRequired": "Для генерации изображений с моделями OpenRouter требуется API-ключ OpenRouter. Настройте его в экспериментальных настройках генерации изображений.", + "failedWithStatus": "Не удалось сгенерировать изображение: {{status}} {{statusText}}", + "failedWithMessage": "Не удалось сгенерировать изображение: {{message}}", + "noImageGenerated": "В ответе не было сгенерировано изображение", + "invalidImageData": "Недопустимые данные изображения в ответе", + "invalidImageFormat": "Получен недопустимый формат изображения", + "unknownError": "Произошла неизвестная ошибка", + "roo": { + "authRequired": "Для генерации изображений требуется аутентификация Roo Code Cloud. Войдите в Roo Code Cloud." + } } } diff --git a/src/i18n/locales/tr/tools.json b/src/i18n/locales/tr/tools.json index 5341a23cb1d..03ab3840091 100644 --- a/src/i18n/locales/tr/tools.json +++ b/src/i18n/locales/tr/tools.json @@ -14,5 +14,17 @@ "errors": { "policy_restriction": "Politika kısıtlamaları nedeniyle yeni görev oluşturulamadı." } + }, + "generateImage": { + "openRouterApiKeyRequired": "OpenRouter modelleriyle görüntü oluşturma için OpenRouter API anahtarı gereklidir. Lütfen Görüntü Oluşturma deneysel ayarlarında yapılandırın.", + "failedWithStatus": "Görüntü oluşturma başarısız: {{status}} {{statusText}}", + "failedWithMessage": "Görüntü oluşturma başarısız: {{message}}", + "noImageGenerated": "Yanıtta görüntü oluşturulmadı", + "invalidImageData": "Yanıtta geçersiz görüntü verisi", + "invalidImageFormat": "Geçersiz görüntü formatı alındı", + "unknownError": "Bilinmeyen bir hata oluştu", + "roo": { + "authRequired": "Görüntü oluşturma için Roo Code Cloud kimlik doğrulaması gereklidir. Lütfen Roo Code Cloud'da oturum açın." + } } } diff --git a/src/i18n/locales/vi/tools.json b/src/i18n/locales/vi/tools.json index 4c5080a1463..3fb845cac8d 100644 --- a/src/i18n/locales/vi/tools.json +++ b/src/i18n/locales/vi/tools.json @@ -14,5 +14,17 @@ "errors": { "policy_restriction": "Không thể tạo nhiệm vụ mới do hạn chế chính sách." } + }, + "generateImage": { + "openRouterApiKeyRequired": "Yêu cầu khóa API OpenRouter để tạo hình ảnh với các mô hình OpenRouter. Vui lòng cấu hình trong cài đặt thử nghiệm Tạo hình ảnh.", + "failedWithStatus": "Tạo hình ảnh thất bại: {{status}} {{statusText}}", + "failedWithMessage": "Tạo hình ảnh thất bại: {{message}}", + "noImageGenerated": "Không có hình ảnh nào được tạo trong phản hồi", + "invalidImageData": "Dữ liệu hình ảnh không hợp lệ trong phản hồi", + "invalidImageFormat": "Nhận được định dạng hình ảnh không hợp lệ", + "unknownError": "Đã xảy ra lỗi không xác định", + "roo": { + "authRequired": "Yêu cầu xác thực Roo Code Cloud để tạo hình ảnh. Vui lòng đăng nhập vào Roo Code Cloud." + } } } diff --git a/src/i18n/locales/zh-CN/tools.json b/src/i18n/locales/zh-CN/tools.json index c0c93d84366..fe9f50a296c 100644 --- a/src/i18n/locales/zh-CN/tools.json +++ b/src/i18n/locales/zh-CN/tools.json @@ -14,5 +14,17 @@ "errors": { "policy_restriction": "由于策略限制,无法创建新任务。" } + }, + "generateImage": { + "openRouterApiKeyRequired": "使用 OpenRouter 模型生成图像需要 OpenRouter API 密钥。请在图像生成实验设置中配置。", + "failedWithStatus": "图像生成失败:{{status}} {{statusText}}", + "failedWithMessage": "图像生成失败:{{message}}", + "noImageGenerated": "响应中未生成图像", + "invalidImageData": "响应中的图像数据无效", + "invalidImageFormat": "收到的图像格式无效", + "unknownError": "发生未知错误", + "roo": { + "authRequired": "图像生成需要 Roo Code Cloud 认证。请登录 Roo Code Cloud。" + } } } diff --git a/src/i18n/locales/zh-TW/tools.json b/src/i18n/locales/zh-TW/tools.json index b736448c20a..1a380bcbeaa 100644 --- a/src/i18n/locales/zh-TW/tools.json +++ b/src/i18n/locales/zh-TW/tools.json @@ -14,5 +14,17 @@ "errors": { "policy_restriction": "由於政策限制,無法建立新工作。" } + }, + "generateImage": { + "openRouterApiKeyRequired": "使用 OpenRouter 模型生成圖像需要 OpenRouter API 金鑰。請在圖像生成實驗設定中配置。", + "failedWithStatus": "圖像生成失敗:{{status}} {{statusText}}", + "failedWithMessage": "圖像生成失敗:{{message}}", + "noImageGenerated": "回應中未生成圖像", + "invalidImageData": "回應中的圖像資料無效", + "invalidImageFormat": "收到的圖像格式無效", + "unknownError": "發生未知錯誤", + "roo": { + "authRequired": "圖像生成需要 Roo Code Cloud 認證。請登入 Roo Code Cloud。" + } } } diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 5b6214b1219..53327df7202 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -283,6 +283,7 @@ export type ExtensionState = Pick< | "profileThresholds" | "includeDiagnosticMessages" | "maxDiagnosticMessages" + | "imageGenerationProvider" | "openRouterImageGenerationSelectedModel" | "includeTaskHistoryInEnhance" | "reasoningBlockCollapsed" diff --git a/webview-ui/src/components/settings/ExperimentalSettings.tsx b/webview-ui/src/components/settings/ExperimentalSettings.tsx index 6883975d02e..fc67c0771b1 100644 --- a/webview-ui/src/components/settings/ExperimentalSettings.tsx +++ b/webview-ui/src/components/settings/ExperimentalSettings.tsx @@ -1,7 +1,7 @@ import { HTMLAttributes } from "react" import { FlaskConical } from "lucide-react" -import type { Experiments } from "@roo-code/types" +import type { Experiments, ImageGenerationProvider } from "@roo-code/types" import { EXPERIMENT_IDS, experimentConfigsMap } from "@roo/experiments" @@ -19,8 +19,10 @@ type ExperimentalSettingsProps = HTMLAttributes & { setExperimentEnabled: SetExperimentEnabled apiConfiguration?: any setApiConfigurationField?: any + imageGenerationProvider?: ImageGenerationProvider openRouterImageApiKey?: string openRouterImageGenerationSelectedModel?: string + setImageGenerationProvider?: (provider: ImageGenerationProvider) => void setOpenRouterImageApiKey?: (apiKey: string) => void setImageGenerationSelectedModel?: (model: string) => void } @@ -30,8 +32,10 @@ export const ExperimentalSettings = ({ setExperimentEnabled, apiConfiguration, setApiConfigurationField, + imageGenerationProvider, openRouterImageApiKey, openRouterImageGenerationSelectedModel, + setImageGenerationProvider, setOpenRouterImageApiKey, setImageGenerationSelectedModel, className, @@ -66,6 +70,7 @@ export const ExperimentalSettings = ({ } if ( config[0] === "IMAGE_GENERATION" && + setImageGenerationProvider && setOpenRouterImageApiKey && setImageGenerationSelectedModel ) { @@ -76,8 +81,10 @@ export const ExperimentalSettings = ({ onChange={(enabled) => setExperimentEnabled(EXPERIMENT_IDS.IMAGE_GENERATION, enabled) } + imageGenerationProvider={imageGenerationProvider} openRouterImageApiKey={openRouterImageApiKey} openRouterImageGenerationSelectedModel={openRouterImageGenerationSelectedModel} + setImageGenerationProvider={setImageGenerationProvider} setOpenRouterImageApiKey={setOpenRouterImageApiKey} setImageGenerationSelectedModel={setImageGenerationSelectedModel} /> diff --git a/webview-ui/src/components/settings/ImageGenerationSettings.tsx b/webview-ui/src/components/settings/ImageGenerationSettings.tsx index 3baa2a9e8be..ccbd0a3fff9 100644 --- a/webview-ui/src/components/settings/ImageGenerationSettings.tsx +++ b/webview-ui/src/components/settings/ImageGenerationSettings.tsx @@ -1,13 +1,15 @@ -import React, { useState, useEffect } from "react" +import React, { useMemo } from "react" import { VSCodeCheckbox, VSCodeTextField, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react" -import { IMAGE_GENERATION_MODELS } from "@roo-code/types" +import { IMAGE_GENERATION_MODELS, type ImageGenerationProvider, getImageGenerationProvider } from "@roo-code/types" import { useAppTranslation } from "@/i18n/TranslationContext" interface ImageGenerationSettingsProps { enabled: boolean onChange: (enabled: boolean) => void + imageGenerationProvider?: ImageGenerationProvider openRouterImageApiKey?: string openRouterImageGenerationSelectedModel?: string + setImageGenerationProvider: (provider: ImageGenerationProvider) => void setOpenRouterImageApiKey: (apiKey: string) => void setImageGenerationSelectedModel: (model: string) => void } @@ -15,36 +17,79 @@ interface ImageGenerationSettingsProps { export const ImageGenerationSettings = ({ enabled, onChange, + imageGenerationProvider, openRouterImageApiKey, openRouterImageGenerationSelectedModel, + setImageGenerationProvider, setOpenRouterImageApiKey, setImageGenerationSelectedModel, }: ImageGenerationSettingsProps) => { const { t } = useAppTranslation() - const [apiKey, setApiKey] = useState(openRouterImageApiKey || "") - const [selectedModel, setSelectedModel] = useState( - openRouterImageGenerationSelectedModel || IMAGE_GENERATION_MODELS[0].value, + // Use shared utility for backwards compatibility logic + const currentProvider = getImageGenerationProvider( + imageGenerationProvider, + !!openRouterImageGenerationSelectedModel, ) - // Update local state when props change (e.g., when switching profiles) - useEffect(() => { - setApiKey(openRouterImageApiKey || "") - setSelectedModel(openRouterImageGenerationSelectedModel || IMAGE_GENERATION_MODELS[0].value) - }, [openRouterImageApiKey, openRouterImageGenerationSelectedModel]) + const availableModels = useMemo(() => { + return IMAGE_GENERATION_MODELS.filter((model) => model.provider === currentProvider) + }, [currentProvider]) + + // Derive the current model value - either from props or first available + const currentModel = useMemo(() => { + // If we have a stored model, verify it exists for the current provider + // (check both value and provider since some models have duplicate values) + if (openRouterImageGenerationSelectedModel) { + // Find a model that matches BOTH the value AND the current provider + const modelInfo = IMAGE_GENERATION_MODELS.find( + (m) => m.value === openRouterImageGenerationSelectedModel && m.provider === currentProvider, + ) + if (modelInfo) { + return openRouterImageGenerationSelectedModel + } + } + // Otherwise use first available model for current provider + return availableModels[0]?.value || IMAGE_GENERATION_MODELS[0].value + }, [openRouterImageGenerationSelectedModel, availableModels, currentProvider]) + + // Handle provider changes + const handleProviderChange = (value: string) => { + const newProvider = value as ImageGenerationProvider + setImageGenerationProvider(newProvider) + + // Smart model selection when switching providers: + // 1. If current model exists for new provider (same model name), keep it + // 2. Otherwise, switch to first available model for new provider + const providerModels = IMAGE_GENERATION_MODELS.filter((m) => m.provider === newProvider) + if (providerModels.length > 0) { + // Check if current model exists for new provider + const currentModelForNewProvider = providerModels.find( + (m) => m.value === openRouterImageGenerationSelectedModel, + ) + if (currentModelForNewProvider) { + // Current model exists for new provider, keep it + // No need to call setImageGenerationSelectedModel since the value doesn't change + } else { + // Current model doesn't exist for new provider, switch to first available + setImageGenerationSelectedModel(providerModels[0].value) + } + } + } // Handle API key changes const handleApiKeyChange = (value: string) => { - setApiKey(value) setOpenRouterImageApiKey(value) } // Handle model selection changes const handleModelChange = (value: string) => { - setSelectedModel(value) setImageGenerationSelectedModel(value) } + const requiresApiKey = currentProvider === "openrouter" + const isConfigured = !requiresApiKey || (requiresApiKey && openRouterImageApiKey) + return (
@@ -60,40 +105,63 @@ export const ImageGenerationSettings = ({ {enabled && (
- {/* API Key Configuration */} + {/* Provider Selection */}
- handleApiKeyChange(e.target.value)} - placeholder={t("settings:experimental.IMAGE_GENERATION.openRouterApiKeyPlaceholder")} - className="w-full" - type="password" - /> + handleProviderChange(e.target.value)} + className="w-full"> + + Roo Code Cloud + + + OpenRouter + +

- {t("settings:experimental.IMAGE_GENERATION.getApiKeyText")}{" "} - - openrouter.ai/keys - + {t("settings:experimental.IMAGE_GENERATION.providerDescription")}

+ {/* API Key Configuration (only for OpenRouter) */} + {currentProvider === "openrouter" && ( +
+ + handleApiKeyChange(e.target.value)} + placeholder={t("settings:experimental.IMAGE_GENERATION.openRouterApiKeyPlaceholder")} + className="w-full" + type="password" + /> +

+ {t("settings:experimental.IMAGE_GENERATION.getApiKeyText")}{" "} + + openrouter.ai/keys + +

+
+ )} + {/* Model Selection */}
handleModelChange(e.target.value)} className="w-full"> - {IMAGE_GENERATION_MODELS.map((model) => ( + {availableModels.map((model) => ( {model.label} @@ -105,13 +173,13 @@ export const ImageGenerationSettings = ({
{/* Status Message */} - {enabled && !apiKey && ( + {enabled && !isConfigured && (
{t("settings:experimental.IMAGE_GENERATION.warningMissingKey")}
)} - {enabled && apiKey && ( + {enabled && isConfigured && (
{t("settings:experimental.IMAGE_GENERATION.successConfigured")}
diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 586aa68c84f..dc1f1ae5ae0 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -32,6 +32,7 @@ import { type ExperimentId, type TelemetrySetting, DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, + ImageGenerationProvider, } from "@roo-code/types" import { vscode } from "@src/utils/vscode" @@ -198,6 +199,7 @@ const SettingsView = forwardRef(({ onDone, t includeDiagnosticMessages, maxDiagnosticMessages, includeTaskHistoryInEnhance, + imageGenerationProvider, openRouterImageApiKey, openRouterImageGenerationSelectedModel, reasoningBlockCollapsed, @@ -289,6 +291,16 @@ const SettingsView = forwardRef(({ onDone, t }) }, []) + const setImageGenerationProvider = useCallback((provider: ImageGenerationProvider) => { + setCachedState((prevState) => { + if (prevState.imageGenerationProvider !== provider) { + setChangeDetected(true) + } + + return { ...prevState, imageGenerationProvider: provider } + }) + }, []) + const setOpenRouterImageApiKey = useCallback((apiKey: string) => { setCachedState((prevState) => { if (prevState.openRouterImageApiKey !== apiKey) { @@ -398,6 +410,7 @@ const SettingsView = forwardRef(({ onDone, t includeCurrentCost: includeCurrentCost ?? true, maxGitStatusFiles: maxGitStatusFiles ?? 0, profileThresholds, + imageGenerationProvider, openRouterImageApiKey, openRouterImageGenerationSelectedModel, experiments, @@ -816,10 +829,12 @@ const SettingsView = forwardRef(({ onDone, t experiments={experiments} apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} + imageGenerationProvider={imageGenerationProvider} openRouterImageApiKey={openRouterImageApiKey as string | undefined} openRouterImageGenerationSelectedModel={ openRouterImageGenerationSelectedModel as string | undefined } + setImageGenerationProvider={setImageGenerationProvider} setOpenRouterImageApiKey={setOpenRouterImageApiKey} setImageGenerationSelectedModel={setImageGenerationSelectedModel} /> diff --git a/webview-ui/src/components/settings/__tests__/ImageGenerationSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/ImageGenerationSettings.spec.tsx index 36504c64f5d..12ad1af591e 100644 --- a/webview-ui/src/components/settings/__tests__/ImageGenerationSettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ImageGenerationSettings.spec.tsx @@ -10,6 +10,7 @@ vi.mock("@/i18n/TranslationContext", () => ({ })) describe("ImageGenerationSettings", () => { + const mockSetImageGenerationProvider = vi.fn() const mockSetOpenRouterImageApiKey = vi.fn() const mockSetImageGenerationSelectedModel = vi.fn() const mockOnChange = vi.fn() @@ -17,8 +18,10 @@ describe("ImageGenerationSettings", () => { const defaultProps = { enabled: false, onChange: mockOnChange, + imageGenerationProvider: undefined, openRouterImageApiKey: undefined, openRouterImageGenerationSelectedModel: undefined, + setImageGenerationProvider: mockSetImageGenerationProvider, setOpenRouterImageApiKey: mockSetOpenRouterImageApiKey, setImageGenerationSelectedModel: mockSetImageGenerationSelectedModel, } @@ -32,6 +35,7 @@ describe("ImageGenerationSettings", () => { render() // Should NOT call setter functions on initial mount to prevent dirty state + expect(mockSetImageGenerationProvider).not.toHaveBeenCalled() expect(mockSetOpenRouterImageApiKey).not.toHaveBeenCalled() expect(mockSetImageGenerationSelectedModel).not.toHaveBeenCalled() }) @@ -46,6 +50,7 @@ describe("ImageGenerationSettings", () => { ) // Should NOT call setter functions on initial mount to prevent dirty state + expect(mockSetImageGenerationProvider).not.toHaveBeenCalled() expect(mockSetOpenRouterImageApiKey).not.toHaveBeenCalled() expect(mockSetImageGenerationSelectedModel).not.toHaveBeenCalled() }) @@ -53,7 +58,10 @@ describe("ImageGenerationSettings", () => { describe("User Interaction Behavior", () => { it("should call setimageGenerationSettings when user changes API key", async () => { - const { getByPlaceholderText } = render() + // Set provider to "openrouter" so the API key field renders + const { getByPlaceholderText } = render( + , + ) const apiKeyInput = getByPlaceholderText( "settings:experimental.IMAGE_GENERATION.openRouterApiKeyPlaceholder", @@ -71,14 +79,27 @@ describe("ImageGenerationSettings", () => { }) describe("Conditional Rendering", () => { - it("should render input fields when enabled is true", () => { - const { getByPlaceholderText } = render() + it("should render input fields when enabled is true and provider is openrouter", () => { + // Set provider to "openrouter" so the API key field renders + const { getByPlaceholderText } = render( + , + ) expect( getByPlaceholderText("settings:experimental.IMAGE_GENERATION.openRouterApiKeyPlaceholder"), ).toBeInTheDocument() }) + it("should not render API key field when provider is roo", () => { + const { queryByPlaceholderText } = render( + , + ) + + expect( + queryByPlaceholderText("settings:experimental.IMAGE_GENERATION.openRouterApiKeyPlaceholder"), + ).not.toBeInTheDocument() + }) + it("should not render input fields when enabled is false", () => { const { queryByPlaceholderText } = render() diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index ac16c70661b..4dffba7a961 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -773,6 +773,8 @@ }, "IMAGE_GENERATION": { "name": "Habilitar generació d'imatges amb IA", + "providerLabel": "Proveïdor", + "providerDescription": "Selecciona el proveïdor per a la generació d'imatges.", "description": "Quan estigui habilitat, Roo pot generar imatges a partir de prompts de text utilitzant els models de generació d'imatges d'OpenRouter. Requereix que es configuri una clau d'API d'OpenRouter.", "openRouterApiKeyLabel": "Clau API d'OpenRouter", "openRouterApiKeyPlaceholder": "Introdueix la teva clau API d'OpenRouter", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 36de5fd2258..162d9c87fef 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -773,7 +773,9 @@ }, "IMAGE_GENERATION": { "name": "KI-Bildgenerierung aktivieren", - "description": "Wenn aktiviert, kann Roo Bilder aus Textprompts mit OpenRouters Bildgenerierungsmodellen erstellen. Erfordert einen konfigurierten OpenRouter API-Schlüssel.", + "description": "Wenn aktiviert, kann Roo Bilder aus Textprompts mit Bildgenerierungsmodellen erstellen.", + "providerLabel": "Anbieter", + "providerDescription": "Wähle den Anbieter für die Bildgenerierung.", "openRouterApiKeyLabel": "OpenRouter API-Schlüssel", "openRouterApiKeyPlaceholder": "Gib deinen OpenRouter API-Schlüssel ein", "getApiKeyText": "Hol dir deinen API-Schlüssel von", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index b77e3e0abf3..8bb526ca708 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -778,13 +778,15 @@ }, "IMAGE_GENERATION": { "name": "Enable AI image generation", - "description": "When enabled, Roo can generate images from text prompts using OpenRouter's image generation models. Requires an OpenRouter API key to be configured.", + "description": "When enabled, Roo can generate images from text prompts using image generation models.", + "providerLabel": "Provider", + "providerDescription": "Select which provider to use for image generation.", "openRouterApiKeyLabel": "OpenRouter API Key", "openRouterApiKeyPlaceholder": "Enter your OpenRouter API key", "getApiKeyText": "Get your API key from", "modelSelectionLabel": "Image Generation Model", "modelSelectionDescription": "Select the model to use for image generation", - "warningMissingKey": "⚠️ OpenRouter API key is required for image generation. Please configure it above.", + "warningMissingKey": "⚠️ OpenRouter API key is required for OpenRouter image generation. Please configure it above.", "successConfigured": "✓ Image generation is configured and ready to use" }, "RUN_SLASH_COMMAND": { diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 580bedf7062..c649a551ceb 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -773,6 +773,8 @@ }, "IMAGE_GENERATION": { "name": "Habilitar generación de imágenes con IA", + "providerLabel": "Proveedor", + "providerDescription": "Selecciona el proveedor para la generación de imágenes.", "description": "Cuando esté habilitado, Roo puede generar imágenes a partir de prompts de texto usando los modelos de generación de imágenes de OpenRouter. Requiere que se configure una clave de API de OpenRouter.", "openRouterApiKeyLabel": "Clave API de OpenRouter", "openRouterApiKeyPlaceholder": "Introduce tu clave API de OpenRouter", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 7593726dabf..b9ce32f4f75 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -773,6 +773,8 @@ }, "IMAGE_GENERATION": { "name": "Activer la génération d'images IA", + "providerLabel": "Fournisseur", + "providerDescription": "Sélectionnez le fournisseur pour la génération d'images.", "description": "Lorsqu'activé, Roo peut générer des images à partir de prompts textuels en utilisant les modèles de génération d'images d'OpenRouter. Nécessite qu'une clé API OpenRouter soit configurée.", "openRouterApiKeyLabel": "Clé API OpenRouter", "openRouterApiKeyPlaceholder": "Entrez votre clé API OpenRouter", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index f28272725ca..cfb6765b9cb 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -773,6 +773,8 @@ "description": "जब सक्षम किया जाता है, तो new_task टूल को todos पैरामीटर प्रदान करने की आवश्यकता होगी। यह सुनिश्चित करता है कि सभी नए कार्य स्पष्ट उद्देश्यों की सूची के साथ शुरू हों। जब अक्षम किया जाता है (डिफ़ॉल्ट), तो todos पैरामीटर पिछड़े संगतता के लिए वैकल्पिक रहता है।" }, "IMAGE_GENERATION": { + "providerLabel": "प्रदाता", + "providerDescription": "छवि निर्माण के लिए प्रदाता चुनें।", "name": "AI छवि निर्माण सक्षम करें", "description": "जब सक्षम किया जाता है, तो Roo OpenRouter के छवि निर्माण मॉडल का उपयोग करके टेक्स्ट प्रॉम्प्ट से छवियां उत्पन्न कर सकता है। एक कॉन्फ़िगर किए गए OpenRouter API कुंजी की आवश्यकता होती है।", "openRouterApiKeyLabel": "OpenRouter API कुंजी", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index a489dadb6cb..78122addcec 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -803,7 +803,9 @@ }, "IMAGE_GENERATION": { "name": "Aktifkan pembuatan gambar AI", - "description": "Ketika diaktifkan, Roo dapat menghasilkan gambar dari prompt teks menggunakan model pembuatan gambar OpenRouter. Memerlukan kunci API OpenRouter yang dikonfigurasi.", + "description": "Ketika diaktifkan, Roo dapat menghasilkan gambar dari prompt teks menggunakan model pembuatan gambar.", + "providerLabel": "Penyedia", + "providerDescription": "Pilih penyedia untuk menghasilkan gambar.", "openRouterApiKeyLabel": "Kunci API OpenRouter", "openRouterApiKeyPlaceholder": "Masukkan kunci API OpenRouter Anda", "getApiKeyText": "Dapatkan kunci API Anda dari", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 02e182a37a4..f1fa03058dd 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -773,6 +773,8 @@ "description": "Quando abilitato, lo strumento new_task richiederà la fornitura di un parametro todos. Ciò garantisce che tutte le nuove attività inizino con un elenco chiaro di obiettivi. Quando disabilitato (impostazione predefinita), il parametro todos rimane facoltativo per la compatibilità con le versioni precedenti." }, "IMAGE_GENERATION": { + "providerLabel": "Provider", + "providerDescription": "Seleziona il provider per la generazione di immagini.", "name": "Abilita generazione immagini AI", "description": "Quando abilitato, Roo può generare immagini da prompt di testo utilizzando i modelli di generazione immagini di OpenRouter. Richiede una chiave API OpenRouter configurata.", "openRouterApiKeyLabel": "Chiave API OpenRouter", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 5a895c7134e..b5aecd04c59 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -773,6 +773,8 @@ "description": "有効にすると、new_taskツールはtodosパラメータの提供が必須になります。これにより、すべての新しいタスクが明確な目的のリストで開始されることが保証されます。無効(デフォルト)の場合、下位互換性のためにtodosパラメータはオプションのままです。" }, "IMAGE_GENERATION": { + "providerLabel": "プロバイダー", + "providerDescription": "画像生成に使用するプロバイダーを選択", "name": "AI画像生成を有効にする", "description": "有効にすると、RooはOpenRouterの画像生成モデルを使用してテキストプロンプトから画像を生成できます。OpenRouter APIキーの設定が必要です。", "openRouterApiKeyLabel": "OpenRouter APIキー", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 12d1aeaf026..46e42a1b11a 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -773,6 +773,8 @@ "description": "활성화하면 new_task 도구에 todos 매개변수를 제공해야 합니다. 이렇게 하면 모든 새 작업이 명확한 목표 목록으로 시작됩니다. 비활성화하면(기본값) 이전 버전과의 호환성을 위해 todos 매개변수는 선택 사항으로 유지됩니다." }, "IMAGE_GENERATION": { + "providerLabel": "제공업체", + "providerDescription": "이미지 생성에 사용할 제공업체를 선택하세요.", "name": "AI 이미지 생성 활성화", "description": "활성화하면 Roo는 OpenRouter의 이미지 생성 모델을 사용하여 텍스트 프롬프트에서 이미지를 생성할 수 있습니다. OpenRouter API 키 구성이 필요합니다.", "openRouterApiKeyLabel": "OpenRouter API 키", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 0258b85210a..63d09f057bd 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -773,6 +773,8 @@ "description": "Wanneer ingeschakeld, vereist de new_task-tool dat een todos-parameter wordt opgegeven. Dit zorgt ervoor dat alle nieuwe taken beginnen met een duidelijke lijst met doelstellingen. Wanneer uitgeschakeld (standaard), blijft de todos-parameter optioneel voor achterwaartse compatibiliteit." }, "IMAGE_GENERATION": { + "providerLabel": "Provider", + "providerDescription": "Selecteer de provider voor het genereren van afbeeldingen.", "name": "AI-afbeeldingsgeneratie inschakelen", "description": "Wanneer ingeschakeld, kan Roo afbeeldingen genereren van tekstprompts met behulp van OpenRouter's afbeeldingsgeneratiemodellen. Vereist een geconfigureerde OpenRouter API-sleutel.", "openRouterApiKeyLabel": "OpenRouter API-sleutel", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 672753a90c0..847199b1815 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -773,6 +773,8 @@ "description": "Gdy włączone, narzędzie new_task będzie wymagało podania parametru todos. Zapewnia to, że wszystkie nowe zadania rozpoczynają się od jasnej listy celów. Gdy wyłączone (domyślnie), parametr todos pozostaje opcjonalny dla zachowania kompatybilności wstecznej." }, "IMAGE_GENERATION": { + "providerLabel": "Dostawca", + "providerDescription": "Wybierz dostawcę do generowania obrazów.", "name": "Włącz generowanie obrazów AI", "description": "Gdy włączone, Roo może generować obrazy z promptów tekstowych używając modeli generowania obrazów OpenRouter. Wymaga skonfigurowanego klucza API OpenRouter.", "openRouterApiKeyLabel": "Klucz API OpenRouter", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 980870c30b4..42e56656498 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -773,6 +773,8 @@ "description": "Quando ativado, a ferramenta new_task exigirá que um parâmetro todos seja fornecido. Isso garante que todas as novas tarefas comecem com uma lista clara de objetivos. Quando desativado (padrão), o parâmetro todos permanece opcional para compatibilidade com versões anteriores." }, "IMAGE_GENERATION": { + "providerLabel": "Provedor", + "providerDescription": "Selecione o provedor para geração de imagens.", "name": "Habilitar geração de imagens com IA", "description": "Quando habilitado, Roo pode gerar imagens a partir de prompts de texto usando os modelos de geração de imagens do OpenRouter. Requer uma chave de API do OpenRouter configurada.", "openRouterApiKeyLabel": "Chave de API do OpenRouter", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 267f083c90a..685d45b2c93 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -773,6 +773,8 @@ "description": "Если включено, инструмент new_task будет требовать предоставления параметра todos. Это гарантирует, что все новые задачи начинаются с четкого списка целей. Когда отключено (по умолчанию), параметр todos остается необязательным для обратной совместимости." }, "IMAGE_GENERATION": { + "providerLabel": "Провайдер", + "providerDescription": "Выберите провайдера для генерации изображений.", "name": "Включить генерацию изображений ИИ", "description": "Когда включено, Roo может генерировать изображения из текстовых запросов, используя модели генерации изображений OpenRouter. Требует настроенный API-ключ OpenRouter.", "openRouterApiKeyLabel": "API-ключ OpenRouter", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index acbf07bc3ab..b3aadba83d0 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -773,6 +773,8 @@ "description": "Etkinleştirildiğinde, new_task aracı bir todos parametresi sağlanmasını gerektirir. Bu, tüm yeni görevlerin net bir hedef listesiyle başlamasını sağlar. Devre dışı bırakıldığında (varsayılan), todos parametresi geriye dönük uyumluluk için isteğe bağlı kalır." }, "IMAGE_GENERATION": { + "providerLabel": "Sağlayıcı", + "providerDescription": "Görüntü oluşturma için kullanılacak sağlayıcıyı seçin.", "name": "AI görüntü üretimini etkinleştir", "description": "Etkinleştirildiğinde, Roo OpenRouter'ın görüntü üretim modellerini kullanarak metin istemlerinden görüntüler üretebilir. Yapılandırılmış bir OpenRouter API anahtarı gerektirir.", "openRouterApiKeyLabel": "OpenRouter API Anahtarı", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index e8d752ca5d6..5422193f53c 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -773,6 +773,8 @@ "description": "Khi được bật, công cụ new_task sẽ yêu cầu cung cấp tham số todos. Điều này đảm bảo tất cả các nhiệm vụ mới bắt đầu với một danh sách mục tiêu rõ ràng. Khi bị tắt (mặc định), tham số todos vẫn là tùy chọn để tương thích ngược." }, "IMAGE_GENERATION": { + "providerLabel": "Nhà cung cấp", + "providerDescription": "Chọn nhà cung cấp để tạo hình ảnh.", "name": "Bật tạo hình ảnh AI", "description": "Khi được bật, Roo có thể tạo hình ảnh từ lời nhắc văn bản bằng các mô hình tạo hình ảnh của OpenRouter. Yêu cầu khóa API OpenRouter được cấu hình.", "openRouterApiKeyLabel": "Khóa API OpenRouter", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 004e4fdc943..ae64917d760 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -773,6 +773,8 @@ "description": "启用后,new_task 工具将需要提供 todos 参数。这可以确保所有新任务都以明确的目标列表开始。禁用时(默认),todos 参数保持可选,以实现向后兼容。" }, "IMAGE_GENERATION": { + "providerLabel": "提供商", + "providerDescription": "选择用于图像生成的提供商。", "name": "启用 AI 图像生成", "description": "启用后,Roo 可以使用 OpenRouter 的图像生成模型从文本提示生成图像。需要配置 OpenRouter API 密钥。", "openRouterApiKeyLabel": "OpenRouter API 密钥", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 14a7095192e..88996066872 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -773,6 +773,8 @@ "description": "啟用後,new_task 工具將需要提供 todos 參數。這可以確保所有新工作都以明確的目標列表開始。停用時(預設),todos 參數保持可選,以實現向後相容。" }, "IMAGE_GENERATION": { + "providerLabel": "提供商", + "providerDescription": "選擇用於圖像生成的提供商。", "name": "啟用 AI 圖像生成", "description": "啟用後,Roo 可以使用 OpenRouter 的圖像生成模型從文字提示生成圖像。需要設定 OpenRouter API 金鑰。", "openRouterApiKeyLabel": "OpenRouter API 金鑰",