feat: add Gemini provider support for image generation

- Create centralized image generation models configuration in types package
- Implement generateImage() method in Gemini provider for text-to-image and image-to-image
- Add provider dropdown in UI settings to switch between OpenRouter and Gemini
- Update generateImageTool to route requests to appropriate provider
- Add settings persistence for Gemini API key and model selection
- Translate all new UI strings across 18 supported languages
- Fix settings state management to properly handle new image generation settings
This commit is contained in:
daniel-lxs 2025-09-12 17:15:30 -05:00 committed by Hannes Rudolph
parent a8f87d2b1d
commit 68a7c8390b
30 changed files with 653 additions and 89 deletions

View file

@ -44,8 +44,11 @@ export const globalSettingsSchema = z.object({
dismissedUpsells: z.array(z.string()).optional(),
// Image generation settings (experimental) - flattened for simplicity
imageGenerationProvider: z.enum(["openrouter", "gemini"]).optional(),
openRouterImageApiKey: z.string().optional(),
openRouterImageGenerationSelectedModel: z.string().optional(),
geminiImageApiKey: z.string().optional(),
geminiImageGenerationSelectedModel: z.string().optional(),
condensingApiConfigId: z.string().optional(),
customCondensingPrompt: z.string().optional(),
@ -210,7 +213,8 @@ export const SECRET_STATE_KEYS = [
// Global secrets that are part of GlobalSettings (not ProviderSettings)
export const GLOBAL_SECRET_KEYS = [
"openRouterImageApiKey", // For image generation
"openRouterImageApiKey", // For OpenRouter image generation
"geminiImageApiKey", // For Gemini image generation
] as const
// Type for the actual secret storage keys

View file

@ -0,0 +1,75 @@
import { z } from "zod"
/**
* Image Generation Provider
*/
export const imageGenerationProviders = ["openrouter", "gemini"] as const
export const imageGenerationProviderSchema = z.enum(imageGenerationProviders)
export type ImageGenerationProvider = z.infer<typeof imageGenerationProviderSchema>
/**
* Image Generation Model Info
*/
export interface ImageGenerationModelInfo {
provider: ImageGenerationProvider
modelId: string
label: string
supportsEditMode?: boolean // Whether the model supports image editing (text + image input)
maxInputSize?: number // Maximum input image size in MB
outputFormats?: string[] // Supported output formats
}
/**
* Image Generation Models by Provider
*/
export const IMAGE_GENERATION_MODELS: Record<ImageGenerationProvider, ImageGenerationModelInfo[]> = {
openrouter: [
{
provider: "openrouter",
modelId: "google/gemini-2.5-flash-image-preview",
label: "Gemini 2.5 Flash Image Preview",
supportsEditMode: true,
outputFormats: ["png", "jpeg"],
},
{
provider: "openrouter",
modelId: "google/gemini-2.5-flash-image-preview:free",
label: "Gemini 2.5 Flash Image Preview (Free)",
supportsEditMode: true,
outputFormats: ["png", "jpeg"],
},
],
gemini: [
{
provider: "gemini",
modelId: "gemini-2.5-flash-image-preview",
label: "Gemini 2.5 Flash Image Preview",
supportsEditMode: true,
outputFormats: ["png", "jpeg"],
},
],
}
/**
* Helper function to get all models for a specific provider
*/
export function getImageGenerationModelsForProvider(provider: ImageGenerationProvider): ImageGenerationModelInfo[] {
return IMAGE_GENERATION_MODELS[provider] || []
}
/**
* Helper function to get all available image generation models
*/
export function getAllImageGenerationModels(): ImageGenerationModelInfo[] {
return Object.values(IMAGE_GENERATION_MODELS).flat()
}
/**
* Image Generation Result
*/
export interface ImageGenerationResult {
success: boolean
imageData?: string // Base64 encoded image data URL
imageFormat?: string // Format of the generated image (png, jpeg, etc.)
error?: string
}

View file

@ -7,6 +7,7 @@ export * from "./experiment.js"
export * from "./followup.js"
export * from "./global-settings.js"
export * from "./history.js"
export * from "./image-generation.js"
export * from "./ipc.js"
export * from "./marketplace.js"
export * from "./mcp.js"

View file

@ -8,7 +8,13 @@ import {
} from "@google/genai"
import type { JWTInput } from "google-auth-library"
import { type ModelInfo, type GeminiModelId, geminiDefaultModelId, geminiModels } from "@roo-code/types"
import {
type ModelInfo,
type GeminiModelId,
geminiDefaultModelId,
geminiModels,
type ImageGenerationResult,
} from "@roo-code/types"
import type { ApiHandlerOptions } from "../../shared/api"
import { safeJsonParse } from "../../shared/safeJsonParse"
@ -335,4 +341,139 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
return totalCost
}
/**
* Generate an image using Gemini's image generation API
* @param prompt The text prompt for image generation
* @param model The model to use for generation
* @param apiKey The Gemini API key (if not using vertex)
* @param inputImage Optional base64 encoded input image data URL for editing
* @returns The generated image data and format, or an error
*/
async generateImage(
prompt: string,
model: string,
apiKey?: string,
inputImage?: string,
): Promise<ImageGenerationResult> {
try {
// Create a temporary client with the provided API key if needed
let client: GoogleGenAI
if (apiKey && !this.options.vertexProjectId) {
// Use provided API key for standard Gemini
client = new GoogleGenAI({ apiKey })
} else {
// Use existing client (either vertex or standard with already configured key)
client = this.client
}
// Prepare the content for generation
const contents: any[] = []
if (inputImage) {
// For image editing mode, include both text and image
const base64Match = inputImage.match(/^data:image\/(png|jpeg|jpg);base64,(.+)$/)
if (!base64Match) {
return {
success: false,
error: "Invalid input image format. Expected base64 data URL.",
}
}
const mimeType = base64Match[1] === "jpg" ? "image/jpeg" : `image/${base64Match[1]}`
const base64Data = base64Match[2]
contents.push({
role: "user",
parts: [
{ text: prompt },
{
inlineData: {
mimeType,
data: base64Data,
},
},
],
})
} else {
// For text-to-image mode
contents.push({
role: "user",
parts: [{ text: prompt }],
})
}
const config: GenerateContentConfig = {
httpOptions: this.options.googleGeminiBaseUrl
? { baseUrl: this.options.googleGeminiBaseUrl }
: undefined,
temperature: 1.0, // Higher temperature for more creative image generation
}
const params: GenerateContentParameters = {
model,
contents,
config,
}
const result = await client.models.generateContent(params)
// Extract the generated image from the response
if (!result.candidates || result.candidates.length === 0) {
return {
success: false,
error: "No candidates returned in the response",
}
}
const candidate = result.candidates[0]
if (!candidate.content || !candidate.content.parts) {
return {
success: false,
error: "No content parts in the response",
}
}
// Find the image part in the response
let imageData: string | undefined
let imageFormat = "png" // Default format
for (const part of candidate.content.parts) {
if (part.inlineData) {
const mimeType = part.inlineData.mimeType
const data = part.inlineData.data
if (mimeType?.startsWith("image/")) {
// Extract format from mime type
imageFormat = mimeType.replace("image/", "").replace("jpeg", "jpg")
// Convert to data URL format
imageData = `data:${mimeType};base64,${data}`
break
}
}
}
if (!imageData) {
return {
success: false,
error: "No image data found in the response",
}
}
return {
success: true,
imageData,
imageFormat,
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"
console.error("Gemini image generation error:", errorMessage)
return {
success: false,
error: `Failed to generate image: ${errorMessage}`,
}
}
}
}

View file

@ -9,9 +9,8 @@ import { getReadablePath } from "../../utils/path"
import { isPathOutsideWorkspace } from "../../utils/pathUtils"
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
import { OpenRouterHandler } from "../../api/providers/openrouter"
// Hardcoded list of image generation models for now
const IMAGE_GENERATION_MODELS = ["google/gemini-2.5-flash-image", "openai/gpt-5-image", "openai/gpt-5-image-mini"]
import { GeminiHandler } from "../../api/providers/gemini"
import { ImageGenerationProvider, getImageGenerationModelsForProvider } from "@roo-code/types"
export async function generateImageTool(
cline: Task,
@ -128,25 +127,60 @@ export async function generateImageTool(
// Check if file is write-protected
const isWriteProtected = cline.rooProtectedController?.isWriteProtected(relPath) || false
// Get OpenRouter API key from global settings (experimental image generation)
const openRouterApiKey = state?.openRouterImageApiKey
// Get the selected provider from settings (default to openrouter)
const selectedProvider = (state?.imageGenerationProvider || "openrouter") as ImageGenerationProvider
if (!openRouterApiKey) {
await cline.say(
"error",
"OpenRouter API key is required for image generation. Please configure it in the Image Generation experimental settings.",
)
pushToolResult(
formatResponse.toolError(
// Get selected model from settings based on provider
let selectedModel: string
let apiKey: string | undefined
if (selectedProvider === "openrouter") {
apiKey = state?.openRouterImageApiKey
if (!apiKey) {
await cline.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.",
),
)
return
}
// Get selected model or use default for OpenRouter
const models = getImageGenerationModelsForProvider("openrouter")
selectedModel =
state?.openRouterImageGenerationSelectedModel ||
(models[0]?.modelId ?? "google/gemini-2.5-flash-image-preview")
} else if (selectedProvider === "gemini") {
// For Gemini, we can use the existing Gemini API key from the provider settings
// Check for a dedicated image generation API key first, then fall back to the provider's API key
apiKey =
state?.geminiImageApiKey ||
(state?.apiConfiguration?.apiProvider === "gemini" ? state?.apiConfiguration?.geminiApiKey : undefined)
if (!apiKey) {
await cline.say(
"error",
"Gemini API key is required for image generation. Please configure it in the Image Generation experimental settings or in the Gemini provider settings.",
)
pushToolResult(
formatResponse.toolError(
"Gemini API key is required for image generation. Please configure it in the Image Generation experimental settings or in the Gemini provider settings.",
),
)
return
}
// Get selected model or use default for Gemini
const models = getImageGenerationModelsForProvider("gemini")
selectedModel =
state?.geminiImageGenerationSelectedModel || (models[0]?.modelId ?? "gemini-2.5-flash-image-preview")
} else {
await cline.say("error", `Unsupported image generation provider: ${selectedProvider}`)
pushToolResult(formatResponse.toolError(`Unsupported image generation provider: ${selectedProvider}`))
return
}
// Get selected model from settings or use default
const selectedModel = state?.openRouterImageGenerationSelectedModel || IMAGE_GENERATION_MODELS[0]
// Determine if the path is outside the workspace
const fullPath = path.resolve(cline.cwd, removeClosingTag("path", relPath))
const isOutsideWorkspace = isPathOutsideWorkspace(fullPath)
@ -176,16 +210,28 @@ export async function generateImageTool(
return
}
// Create a temporary OpenRouter handler with minimal options
const openRouterHandler = new OpenRouterHandler({} as any)
// Generate image based on provider
let result
// Call the generateImage method with the explicit API key and optional input image
const result = await openRouterHandler.generateImage(
prompt,
selectedModel,
openRouterApiKey,
inputImageData,
)
if (selectedProvider === "openrouter") {
// Create a temporary OpenRouter handler with minimal options
const openRouterHandler = new OpenRouterHandler({} as any)
// Call the generateImage method with the explicit API key and optional input image
result = await openRouterHandler.generateImage(prompt, selectedModel, apiKey!, inputImageData)
} else if (selectedProvider === "gemini") {
// Create a temporary Gemini handler with minimal options
const geminiHandler = new GeminiHandler({ geminiApiKey: apiKey } as any)
// Call the generateImage method with the optional input image
result = await geminiHandler.generateImage(prompt, selectedModel, apiKey, inputImageData)
} else {
// This should not happen due to earlier check, but for type safety
result = {
success: false,
error: `Unsupported provider: ${selectedProvider}`,
}
}
if (!result.success) {
await cline.say("error", result.error || "Failed to generate image")

View file

@ -1811,8 +1811,11 @@ export class ClineProvider
includeTaskHistoryInEnhance,
taskSyncEnabled,
remoteControlEnabled,
imageGenerationProvider,
openRouterImageApiKey,
openRouterImageGenerationSelectedModel,
geminiImageApiKey,
geminiImageGenerationSelectedModel,
openRouterUseMiddleOutTransform,
featureRoomoteControlEnabled,
} = await this.getState()
@ -1960,8 +1963,11 @@ export class ClineProvider
includeTaskHistoryInEnhance: includeTaskHistoryInEnhance ?? true,
taskSyncEnabled,
remoteControlEnabled,
imageGenerationProvider,
openRouterImageApiKey,
openRouterImageGenerationSelectedModel,
geminiImageApiKey,
geminiImageGenerationSelectedModel,
openRouterUseMiddleOutTransform,
featureRoomoteControlEnabled,
}
@ -2181,8 +2187,11 @@ export class ClineProvider
return false
}
})(),
imageGenerationProvider: stateValues.imageGenerationProvider,
openRouterImageApiKey: stateValues.openRouterImageApiKey,
openRouterImageGenerationSelectedModel: stateValues.openRouterImageGenerationSelectedModel,
geminiImageApiKey: stateValues.geminiImageApiKey,
geminiImageGenerationSelectedModel: stateValues.geminiImageGenerationSelectedModel,
featureRoomoteControlEnabled: (() => {
try {
const userSettings = CloudService.instance.getUserSettings()

View file

@ -1578,6 +1578,21 @@ export const webviewMessageHandler = async (
await provider.contextProxy.setValue("openRouterImageGenerationSelectedModel", message.text)
await provider.postStateToWebview()
break
case "imageGenerationProvider":
await provider.contextProxy.setValue(
"imageGenerationProvider",
message.text as "openrouter" | "gemini" | undefined,
)
await provider.postStateToWebview()
break
case "geminiImageApiKey":
await provider.contextProxy.setValue("geminiImageApiKey", message.text)
await provider.postStateToWebview()
break
case "geminiImageGenerationSelectedModel":
await provider.contextProxy.setValue("geminiImageGenerationSelectedModel", message.text)
await provider.postStateToWebview()
break
case "showRooIgnoredFiles":
await updateGlobalState("showRooIgnoredFiles", message.bool ?? false)
await provider.postStateToWebview()

View file

@ -343,6 +343,9 @@ export type ExtensionState = Pick<
profileThresholds: Record<string, number>
hasOpenedModeSelector: boolean
openRouterImageApiKey?: string
imageGenerationProvider?: "openrouter" | "gemini"
geminiImageApiKey?: string
geminiImageGenerationSelectedModel?: string
openRouterUseMiddleOutTransform?: boolean
messageQueue?: QueuedMessage[]
lastShownAnnouncementId?: string

View file

@ -224,6 +224,9 @@ export interface WebviewMessage {
| "imageGenerationSettings"
| "openRouterImageApiKey"
| "openRouterImageGenerationSelectedModel"
| "imageGenerationProvider"
| "geminiImageApiKey"
| "geminiImageGenerationSelectedModel"
| "queueMessage"
| "removeQueuedMessage"
| "editQueuedMessage"

View file

@ -21,8 +21,13 @@ type ExperimentalSettingsProps = HTMLAttributes<HTMLDivElement> & {
setApiConfigurationField?: any
openRouterImageApiKey?: string
openRouterImageGenerationSelectedModel?: string
imageGenerationProvider?: string
geminiImageApiKey?: string
geminiImageGenerationSelectedModel?: string
setOpenRouterImageApiKey?: (apiKey: string) => void
setImageGenerationSelectedModel?: (model: string) => void
setImageGenerationProvider?: (provider: "gemini" | "openrouter" | undefined) => void
setGeminiImageApiKey?: (apiKey: string) => void
}
export const ExperimentalSettings = ({
@ -32,8 +37,13 @@ export const ExperimentalSettings = ({
setApiConfigurationField,
openRouterImageApiKey,
openRouterImageGenerationSelectedModel,
imageGenerationProvider,
geminiImageApiKey,
geminiImageGenerationSelectedModel,
setOpenRouterImageApiKey,
setImageGenerationSelectedModel,
setImageGenerationProvider,
setGeminiImageApiKey,
className,
...props
}: ExperimentalSettingsProps) => {
@ -78,8 +88,13 @@ export const ExperimentalSettings = ({
}
openRouterImageApiKey={openRouterImageApiKey}
openRouterImageGenerationSelectedModel={openRouterImageGenerationSelectedModel}
imageGenerationProvider={imageGenerationProvider as any}
geminiApiKey={geminiImageApiKey}
geminiImageGenerationSelectedModel={geminiImageGenerationSelectedModel}
setOpenRouterImageApiKey={setOpenRouterImageApiKey}
setImageGenerationSelectedModel={setImageGenerationSelectedModel}
setImageGenerationProvider={setImageGenerationProvider}
setGeminiApiKey={setGeminiImageApiKey}
/>
)
}

View file

@ -1,57 +1,118 @@
import React, { useState, useEffect } from "react"
import { VSCodeCheckbox, VSCodeTextField, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { getImageGenerationModelsForProvider, type ImageGenerationProvider } from "@roo-code/types"
interface ImageGenerationSettingsProps {
enabled: boolean
onChange: (enabled: boolean) => void
openRouterImageApiKey?: string
geminiApiKey?: string
openRouterImageGenerationSelectedModel?: string
geminiImageGenerationSelectedModel?: string
imageGenerationProvider?: ImageGenerationProvider
setOpenRouterImageApiKey: (apiKey: string) => void
setGeminiApiKey?: (apiKey: string) => void
setImageGenerationSelectedModel: (model: string) => void
setImageGenerationProvider?: (provider: ImageGenerationProvider | undefined) => void
}
// Hardcoded list of image generation models
const IMAGE_GENERATION_MODELS = [
{ value: "google/gemini-2.5-flash-image", label: "Gemini 2.5 Flash Image" },
{ value: "openai/gpt-5-image", label: "GPT-5 Image" },
{ value: "openai/gpt-5-image-mini", label: "GPT-5 Image Mini" },
// Add more models as they become available
]
export const ImageGenerationSettings = ({
enabled,
onChange,
openRouterImageApiKey,
geminiApiKey,
openRouterImageGenerationSelectedModel,
geminiImageGenerationSelectedModel,
imageGenerationProvider = "openrouter",
setOpenRouterImageApiKey,
setGeminiApiKey,
setImageGenerationSelectedModel,
setImageGenerationProvider,
}: ImageGenerationSettingsProps) => {
const { t } = useAppTranslation()
const [apiKey, setApiKey] = useState(openRouterImageApiKey || "")
const [selectedModel, setSelectedModel] = useState(
openRouterImageGenerationSelectedModel || IMAGE_GENERATION_MODELS[0].value,
)
const [provider, setProvider] = useState<ImageGenerationProvider>(imageGenerationProvider)
const [openRouterApiKeyLocal, setOpenRouterApiKeyLocal] = useState(openRouterImageApiKey || "")
const [geminiApiKeyLocal, setGeminiApiKeyLocal] = useState(geminiApiKey || "")
// Get models for current provider
const availableModels = getImageGenerationModelsForProvider(provider)
// Get default model for provider
const getDefaultModel = (provider: ImageGenerationProvider) => {
const models = getImageGenerationModelsForProvider(provider)
return models.length > 0 ? models[0].modelId : ""
}
// Get current selected model based on provider
const getCurrentSelectedModel = () => {
if (provider === "openrouter") {
return openRouterImageGenerationSelectedModel || getDefaultModel("openrouter")
} else {
return geminiImageGenerationSelectedModel || getDefaultModel("gemini")
}
}
const [selectedModel, setSelectedModel] = useState(getCurrentSelectedModel())
// Update local state when props change (e.g., when switching profiles)
useEffect(() => {
setApiKey(openRouterImageApiKey || "")
setSelectedModel(openRouterImageGenerationSelectedModel || IMAGE_GENERATION_MODELS[0].value)
}, [openRouterImageApiKey, openRouterImageGenerationSelectedModel])
setOpenRouterApiKeyLocal(openRouterImageApiKey || "")
setGeminiApiKeyLocal(geminiApiKey || "")
setProvider(imageGenerationProvider || "openrouter")
// Calculate selected model directly in the effect
const newSelectedModel =
provider === "openrouter"
? openRouterImageGenerationSelectedModel || getDefaultModel("openrouter")
: geminiImageGenerationSelectedModel || getDefaultModel("gemini")
setSelectedModel(newSelectedModel)
}, [
openRouterImageApiKey,
geminiApiKey,
openRouterImageGenerationSelectedModel,
geminiImageGenerationSelectedModel,
imageGenerationProvider,
provider,
])
// Handle provider change
const handleProviderChange = (value: ImageGenerationProvider) => {
setProvider(value)
if (setImageGenerationProvider) {
setImageGenerationProvider(value as ImageGenerationProvider | undefined)
}
// Set default model for new provider
const defaultModel = getDefaultModel(value)
setSelectedModel(defaultModel)
setImageGenerationSelectedModel(defaultModel)
}
// Handle API key changes
const handleApiKeyChange = (value: string) => {
setApiKey(value)
const handleOpenRouterApiKeyChange = (value: string) => {
setOpenRouterApiKeyLocal(value)
setOpenRouterImageApiKey(value)
}
const handleGeminiApiKeyChange = (value: string) => {
setGeminiApiKeyLocal(value)
if (setGeminiApiKey) {
setGeminiApiKey(value)
}
}
// Handle model selection changes
const handleModelChange = (value: string) => {
setSelectedModel(value)
setImageGenerationSelectedModel(value)
}
// Get current API key based on provider
const getCurrentApiKey = () => {
return provider === "openrouter" ? openRouterApiKeyLocal : geminiApiKeyLocal
}
return (
<div className="space-y-4">
<div>
@ -67,30 +128,78 @@ export const ImageGenerationSettings = ({
{enabled && (
<div className="ml-2 space-y-3">
{/* API Key Configuration */}
{/* Provider Selection */}
<div>
<label className="block font-medium mb-1">
{t("settings:experimental.IMAGE_GENERATION.openRouterApiKeyLabel")}
{t("settings:experimental.IMAGE_GENERATION.providerLabel")}
</label>
<VSCodeTextField
value={apiKey}
onInput={(e: any) => handleApiKeyChange(e.target.value)}
placeholder={t("settings:experimental.IMAGE_GENERATION.openRouterApiKeyPlaceholder")}
className="w-full"
type="password"
/>
<VSCodeDropdown
value={provider}
onChange={(e: any) => handleProviderChange(e.target.value as ImageGenerationProvider)}
className="w-full">
<VSCodeOption value="openrouter" className="py-2 px-3">
OpenRouter
</VSCodeOption>
<VSCodeOption value="gemini" className="py-2 px-3">
Gemini
</VSCodeOption>
</VSCodeDropdown>
<p className="text-vscode-descriptionForeground text-xs mt-1">
{t("settings:experimental.IMAGE_GENERATION.getApiKeyText")}{" "}
<a
href="https://openrouter.ai/keys"
target="_blank"
rel="noopener noreferrer"
className="text-vscode-textLink-foreground hover:text-vscode-textLink-activeForeground">
openrouter.ai/keys
</a>
{t("settings:experimental.IMAGE_GENERATION.providerDescription")}
</p>
</div>
{/* API Key Configuration based on provider */}
{provider === "openrouter" && (
<div>
<label className="block font-medium mb-1">
{t("settings:experimental.IMAGE_GENERATION.openRouterApiKeyLabel")}
</label>
<VSCodeTextField
value={openRouterApiKeyLocal}
onInput={(e: any) => handleOpenRouterApiKeyChange(e.target.value)}
placeholder={t("settings:experimental.IMAGE_GENERATION.openRouterApiKeyPlaceholder")}
className="w-full"
type="password"
/>
<p className="text-vscode-descriptionForeground text-xs mt-1">
{t("settings:experimental.IMAGE_GENERATION.getApiKeyText")}{" "}
<a
href="https://openrouter.ai/keys"
target="_blank"
rel="noopener noreferrer"
className="text-vscode-textLink-foreground hover:text-vscode-textLink-activeForeground">
openrouter.ai/keys
</a>
</p>
</div>
)}
{provider === "gemini" && (
<div>
<label className="block font-medium mb-1">
{t("settings:experimental.IMAGE_GENERATION.geminiApiKeyLabel")}
</label>
<VSCodeTextField
value={geminiApiKeyLocal}
onInput={(e: any) => handleGeminiApiKeyChange(e.target.value)}
placeholder={t("settings:experimental.IMAGE_GENERATION.geminiApiKeyPlaceholder")}
className="w-full"
type="password"
/>
<p className="text-vscode-descriptionForeground text-xs mt-1">
{t("settings:experimental.IMAGE_GENERATION.geminiGetApiKeyText")}{" "}
<a
href="https://aistudio.google.com/app/apikey"
target="_blank"
rel="noopener noreferrer"
className="text-vscode-textLink-foreground hover:text-vscode-textLink-activeForeground">
Google AI Studio
</a>
</p>
</div>
)}
{/* Model Selection */}
<div>
<label className="block font-medium mb-1">
@ -100,8 +209,8 @@ export const ImageGenerationSettings = ({
value={selectedModel}
onChange={(e: any) => handleModelChange(e.target.value)}
className="w-full">
{IMAGE_GENERATION_MODELS.map((model) => (
<VSCodeOption key={model.value} value={model.value} className="py-2 px-3">
{availableModels.map((model) => (
<VSCodeOption key={model.modelId} value={model.modelId} className="py-2 px-3">
{model.label}
</VSCodeOption>
))}
@ -112,13 +221,15 @@ export const ImageGenerationSettings = ({
</div>
{/* Status Message */}
{enabled && !apiKey && (
{enabled && !getCurrentApiKey() && (
<div className="p-2 bg-vscode-editorWarning-background text-vscode-editorWarning-foreground rounded text-sm">
{t("settings:experimental.IMAGE_GENERATION.warningMissingKey")}
{provider === "openrouter"
? t("settings:experimental.IMAGE_GENERATION.warningMissingKey")
: t("settings:experimental.IMAGE_GENERATION.warningMissingGeminiKey")}
</div>
)}
{enabled && apiKey && (
{enabled && getCurrentApiKey() && (
<div className="p-2 bg-vscode-editorInfo-background text-vscode-editorInfo-foreground rounded text-sm">
{t("settings:experimental.IMAGE_GENERATION.successConfigured")}
</div>

View file

@ -195,6 +195,9 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
openRouterImageApiKey,
openRouterImageGenerationSelectedModel,
reasoningBlockCollapsed,
imageGenerationProvider,
geminiImageApiKey,
geminiImageGenerationSelectedModel,
} = cachedState
const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
@ -292,11 +295,28 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
const setImageGenerationSelectedModel = useCallback((model: string) => {
setCachedState((prevState) => {
// Only set change detected if value actually changed
if (prevState.openRouterImageGenerationSelectedModel !== model) {
setChangeDetected(true)
setChangeDetected(true)
// Update the appropriate model field based on the current provider
const provider = prevState.imageGenerationProvider || "openrouter"
if (provider === "openrouter") {
return { ...prevState, openRouterImageGenerationSelectedModel: model }
} else {
return { ...prevState, geminiImageGenerationSelectedModel: model }
}
return { ...prevState, openRouterImageGenerationSelectedModel: model }
})
}, [])
const setImageGenerationProvider = useCallback((provider: "gemini" | "openrouter" | undefined) => {
setCachedState((prevState) => {
setChangeDetected(true)
return { ...prevState, imageGenerationProvider: provider }
})
}, [])
const setGeminiImageApiKey = useCallback((apiKey: string) => {
setCachedState((prevState) => {
setChangeDetected(true)
return { ...prevState, geminiImageApiKey: apiKey }
})
}, [])
@ -392,6 +412,12 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
type: "openRouterImageGenerationSelectedModel",
text: openRouterImageGenerationSelectedModel,
})
vscode.postMessage({ type: "imageGenerationProvider", text: imageGenerationProvider })
vscode.postMessage({ type: "geminiImageApiKey", text: geminiImageApiKey })
vscode.postMessage({
type: "geminiImageGenerationSelectedModel",
text: geminiImageGenerationSelectedModel,
})
setChangeDetected(false)
}
}
@ -797,8 +823,15 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
openRouterImageGenerationSelectedModel={
openRouterImageGenerationSelectedModel as string | undefined
}
imageGenerationProvider={imageGenerationProvider as string | undefined}
geminiImageApiKey={geminiImageApiKey as string | undefined}
geminiImageGenerationSelectedModel={
geminiImageGenerationSelectedModel as string | undefined
}
setOpenRouterImageApiKey={setOpenRouterImageApiKey}
setImageGenerationSelectedModel={setImageGenerationSelectedModel}
setImageGenerationProvider={setImageGenerationProvider}
setGeminiImageApiKey={setGeminiImageApiKey}
/>
)}

View file

@ -749,7 +749,13 @@
"modelSelectionLabel": "Model de generació d'imatges",
"modelSelectionDescription": "Selecciona el model per a la generació d'imatges",
"warningMissingKey": "⚠️ La clau API d'OpenRouter és necessària per a la generació d'imatges. Si us plau, configura-la a dalt.",
"successConfigured": "✓ La generació d'imatges està configurada i llesta per utilitzar"
"successConfigured": "✓ La generació d'imatges està configurada i llesta per utilitzar",
"providerLabel": "Proveïdor de generació d'imatges",
"providerDescription": "Tria el proveïdor per a la generació d'imatges",
"geminiApiKeyLabel": "Clau API de Gemini",
"geminiApiKeyPlaceholder": "Introdueix la teva clau API de Gemini",
"geminiGetApiKeyText": "Obté la teva clau API de",
"warningMissingGeminiKey": "⚠️ La clau API de Gemini és necessària per a la generació d'imatges. Si us plau, configura-la a dalt."
},
"RUN_SLASH_COMMAND": {
"name": "Habilitar comandes de barra diagonal iniciades pel model",

View file

@ -749,7 +749,13 @@
"modelSelectionLabel": "Bildgenerierungsmodell",
"modelSelectionDescription": "Wähle das Modell für die Bildgenerierung aus",
"warningMissingKey": "⚠️ OpenRouter API-Schlüssel ist für Bildgenerierung erforderlich. Bitte konfiguriere ihn oben.",
"successConfigured": "✓ Bildgenerierung ist konfiguriert und einsatzbereit"
"successConfigured": "✓ Bildgenerierung ist konfiguriert und einsatzbereit",
"providerLabel": "Bildgenerierungs-Anbieter",
"providerDescription": "Wähle den Anbieter für die Bildgenerierung",
"geminiApiKeyLabel": "Gemini API-Schlüssel",
"geminiApiKeyPlaceholder": "Gib deinen Gemini API-Schlüssel ein",
"geminiGetApiKeyText": "Hol dir deinen API-Schlüssel von",
"warningMissingGeminiKey": "⚠️ Gemini API-Schlüssel ist für Bildgenerierung erforderlich. Bitte konfiguriere ihn oben."
},
"RUN_SLASH_COMMAND": {
"name": "Modellinitierte Slash-Befehle aktivieren",

View file

@ -754,7 +754,13 @@
"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.",
"successConfigured": "✓ Image generation is configured and ready to use"
"successConfigured": "✓ Image generation is configured and ready to use",
"providerLabel": "Image Generation Provider",
"providerDescription": "Choose the provider for image generation",
"geminiApiKeyLabel": "Gemini API Key",
"geminiApiKeyPlaceholder": "Enter your Gemini API key",
"geminiGetApiKeyText": "Get your API key from",
"warningMissingGeminiKey": "⚠️ Gemini API key is required for image generation. Please configure it above."
},
"RUN_SLASH_COMMAND": {
"name": "Enable model-initiated slash commands",

View file

@ -749,7 +749,13 @@
"modelSelectionLabel": "Modelo de generación de imágenes",
"modelSelectionDescription": "Selecciona el modelo para la generación de imágenes",
"warningMissingKey": "⚠️ La clave API de OpenRouter es requerida para la generación de imágenes. Por favor, configúrala arriba.",
"successConfigured": "✓ La generación de imágenes está configurada y lista para usar"
"successConfigured": "✓ La generación de imágenes está configurada y lista para usar",
"providerLabel": "Proveedor de generación de imágenes",
"providerDescription": "Elige el proveedor para la generación de imágenes",
"geminiApiKeyLabel": "Clave API de Gemini",
"geminiApiKeyPlaceholder": "Introduce tu clave API de Gemini",
"geminiGetApiKeyText": "Obtén tu clave API de",
"warningMissingGeminiKey": "⚠️ La clave API de Gemini es requerida para la generación de imágenes. Por favor, configúrala arriba."
},
"RUN_SLASH_COMMAND": {
"name": "Habilitar comandos slash iniciados por el modelo",

View file

@ -749,7 +749,13 @@
"modelSelectionLabel": "Modèle de génération d'images",
"modelSelectionDescription": "Sélectionnez le modèle pour la génération d'images",
"warningMissingKey": "⚠️ Une clé API OpenRouter est requise pour la génération d'images. Veuillez la configurer ci-dessus.",
"successConfigured": "✓ La génération d'images est configurée et prête à utiliser"
"successConfigured": "✓ La génération d'images est configurée et prête à utiliser",
"providerLabel": "Fournisseur de génération d'images",
"providerDescription": "Choisissez le fournisseur pour la génération d'images",
"geminiApiKeyLabel": "Clé API Gemini",
"geminiApiKeyPlaceholder": "Entrez votre clé API Gemini",
"geminiGetApiKeyText": "Obtenez votre clé API depuis",
"warningMissingGeminiKey": "⚠️ Une clé API Gemini est requise pour la génération d'images. Veuillez la configurer ci-dessus."
},
"RUN_SLASH_COMMAND": {
"name": "Activer les commandes slash initiées par le modèle",

View file

@ -750,7 +750,13 @@
"modelSelectionLabel": "छवि निर्माण मॉडल",
"modelSelectionDescription": "छवि निर्माण के लिए उपयोग करने वाला मॉडल चुनें",
"warningMissingKey": "⚠️ छवि निर्माण के लिए OpenRouter API कुंजी आवश्यक है। कृपया इसे ऊपर कॉन्फ़िगर करें।",
"successConfigured": "✓ छवि निर्माण कॉन्फ़िगर है और उपयोग के लिए तैयार है"
"successConfigured": "✓ छवि निर्माण कॉन्फ़िगर है और उपयोग के लिए तैयार है",
"providerLabel": "छवि निर्माण प्रदाता",
"providerDescription": "छवि निर्माण के लिए प्रदाता चुनें",
"geminiApiKeyLabel": "Gemini API कुंजी",
"geminiApiKeyPlaceholder": "अपनी Gemini API कुंजी दर्ज करें",
"geminiGetApiKeyText": "अपनी API कुंजी प्राप्त करें",
"warningMissingGeminiKey": "⚠️ छवि निर्माण के लिए Gemini API कुंजी आवश्यक है। कृपया इसे ऊपर कॉन्फ़िगर करें।"
},
"RUN_SLASH_COMMAND": {
"name": "मॉडल द्वारा शुरू किए गए स्लैश कमांड सक्षम करें",

View file

@ -779,7 +779,13 @@
"modelSelectionLabel": "Model Pembuatan Gambar",
"modelSelectionDescription": "Pilih model untuk pembuatan gambar",
"warningMissingKey": "⚠️ Kunci API OpenRouter diperlukan untuk pembuatan gambar. Silakan konfigurasi di atas.",
"successConfigured": "✓ Pembuatan gambar dikonfigurasi dan siap digunakan"
"successConfigured": "✓ Pembuatan gambar dikonfigurasi dan siap digunakan",
"providerLabel": "Penyedia pembuatan gambar",
"providerDescription": "Pilih penyedia untuk pembuatan gambar",
"geminiApiKeyLabel": "Kunci API Gemini",
"geminiApiKeyPlaceholder": "Masukkan kunci API Gemini Anda",
"geminiGetApiKeyText": "Dapatkan kunci API Anda dari",
"warningMissingGeminiKey": "⚠️ Kunci API Gemini diperlukan untuk pembuatan gambar. Silakan konfigurasi di atas."
},
"RUN_SLASH_COMMAND": {
"name": "Aktifkan perintah slash yang dimulai model",

View file

@ -750,7 +750,13 @@
"modelSelectionLabel": "Modello di generazione immagini",
"modelSelectionDescription": "Seleziona il modello per la generazione di immagini",
"warningMissingKey": "⚠️ La chiave API OpenRouter è richiesta per la generazione di immagini. Configurala sopra.",
"successConfigured": "✓ La generazione di immagini è configurata e pronta per l'uso"
"successConfigured": "✓ La generazione di immagini è configurata e pronta per l'uso",
"providerLabel": "Provider di generazione immagini",
"providerDescription": "Scegli il provider per la generazione di immagini",
"geminiApiKeyLabel": "Chiave API Gemini",
"geminiApiKeyPlaceholder": "Inserisci la tua chiave API Gemini",
"geminiGetApiKeyText": "Ottieni la tua chiave API da",
"warningMissingGeminiKey": "⚠️ La chiave API Gemini è richiesta per la generazione di immagini. Configurala sopra."
},
"RUN_SLASH_COMMAND": {
"name": "Abilita comandi slash avviati dal modello",

View file

@ -750,7 +750,13 @@
"modelSelectionLabel": "画像生成モデル",
"modelSelectionDescription": "画像生成に使用するモデルを選択",
"warningMissingKey": "⚠️ 画像生成にはOpenRouter APIキーが必要です。上記で設定してください。",
"successConfigured": "✓ 画像生成が設定され、使用準備完了です"
"successConfigured": "✓ 画像生成が設定され、使用準備完了です",
"providerLabel": "画像生成プロバイダー",
"providerDescription": "画像生成のプロバイダーを選択",
"geminiApiKeyLabel": "Gemini APIキー",
"geminiApiKeyPlaceholder": "Gemini APIキーを入力してください",
"geminiGetApiKeyText": "APIキーを取得する場所",
"warningMissingGeminiKey": "⚠️ 画像生成にはGemini APIキーが必要です。上記で設定してください。"
},
"RUN_SLASH_COMMAND": {
"name": "モデル開始スラッシュコマンドを有効にする",

View file

@ -750,7 +750,13 @@
"modelSelectionLabel": "이미지 생성 모델",
"modelSelectionDescription": "이미지 생성에 사용할 모델을 선택하세요",
"warningMissingKey": "⚠️ 이미지 생성에는 OpenRouter API 키가 필요합니다. 위에서 설정해주세요.",
"successConfigured": "✓ 이미지 생성이 구성되었으며 사용할 준비가 되었습니다"
"successConfigured": "✓ 이미지 생성이 구성되었으며 사용할 준비가 되었습니다",
"providerLabel": "이미지 생성 제공자",
"providerDescription": "이미지 생성 제공자를 선택하세요",
"geminiApiKeyLabel": "Gemini API 키",
"geminiApiKeyPlaceholder": "Gemini API 키를 입력하세요",
"geminiGetApiKeyText": "API 키를 받을 곳",
"warningMissingGeminiKey": "⚠️ 이미지 생성에는 Gemini API 키가 필요합니다. 위에서 설정해주세요."
},
"RUN_SLASH_COMMAND": {
"name": "모델 시작 슬래시 명령 활성화",

View file

@ -750,7 +750,13 @@
"modelSelectionLabel": "Afbeeldingsgeneratiemodel",
"modelSelectionDescription": "Selecteer het model voor afbeeldingsgeneratie",
"warningMissingKey": "⚠️ OpenRouter API-sleutel is vereist voor afbeeldingsgeneratie. Configureer deze hierboven.",
"successConfigured": "✓ Afbeeldingsgeneratie is geconfigureerd en klaar voor gebruik"
"successConfigured": "✓ Afbeeldingsgeneratie is geconfigureerd en klaar voor gebruik",
"providerLabel": "Afbeeldingsgeneratie provider",
"providerDescription": "Kies de provider voor afbeeldingsgeneratie",
"geminiApiKeyLabel": "Gemini API-sleutel",
"geminiApiKeyPlaceholder": "Voer je Gemini API-sleutel in",
"geminiGetApiKeyText": "Haal je API-sleutel op van",
"warningMissingGeminiKey": "⚠️ Gemini API-sleutel is vereist voor afbeeldingsgeneratie. Configureer deze hierboven."
},
"RUN_SLASH_COMMAND": {
"name": "Model-geïnitieerde slash-commando's inschakelen",

View file

@ -750,7 +750,13 @@
"modelSelectionLabel": "Model generowania obrazów",
"modelSelectionDescription": "Wybierz model do generowania obrazów",
"warningMissingKey": "⚠️ Klucz API OpenRouter jest wymagany do generowania obrazów. Skonfiguruj go powyżej.",
"successConfigured": "✓ Generowanie obrazów jest skonfigurowane i gotowe do użycia"
"successConfigured": "✓ Generowanie obrazów jest skonfigurowane i gotowe do użycia",
"providerLabel": "Dostawca generowania obrazów",
"providerDescription": "Wybierz dostawcę do generowania obrazów",
"geminiApiKeyLabel": "Klucz API Gemini",
"geminiApiKeyPlaceholder": "Wprowadź swój klucz API Gemini",
"geminiGetApiKeyText": "Uzyskaj swój klucz API od",
"warningMissingGeminiKey": "⚠️ Klucz API Gemini jest wymagany do generowania obrazów. Skonfiguruj go powyżej."
},
"RUN_SLASH_COMMAND": {
"name": "Włącz polecenia slash inicjowane przez model",

View file

@ -750,7 +750,13 @@
"modelSelectionLabel": "Modelo de Geração de Imagens",
"modelSelectionDescription": "Selecione o modelo para geração de imagens",
"warningMissingKey": "⚠️ A chave de API do OpenRouter é necessária para geração de imagens. Configure-a acima.",
"successConfigured": "✓ A geração de imagens está configurada e pronta para uso"
"successConfigured": "✓ A geração de imagens está configurada e pronta para uso",
"providerLabel": "Provedor de geração de imagens",
"providerDescription": "Escolha o provedor para geração de imagens",
"geminiApiKeyLabel": "Chave de API do Gemini",
"geminiApiKeyPlaceholder": "Digite sua chave de API do Gemini",
"geminiGetApiKeyText": "Obtenha sua chave de API de",
"warningMissingGeminiKey": "⚠️ A chave de API do Gemini é necessária para geração de imagens. Configure-a acima."
},
"RUN_SLASH_COMMAND": {
"name": "Ativar comandos slash iniciados pelo modelo",

View file

@ -750,7 +750,13 @@
"modelSelectionLabel": "Модель генерации изображений",
"modelSelectionDescription": "Выберите модель для генерации изображений",
"warningMissingKey": "⚠️ API-ключ OpenRouter необходим для генерации изображений. Настройте его выше.",
"successConfigured": "✓ Генерация изображений настроена и готова к использованию"
"successConfigured": "✓ Генерация изображений настроена и готова к использованию",
"providerLabel": "Провайдер генерации изображений",
"providerDescription": "Выберите провайдера для генерации изображений",
"geminiApiKeyLabel": "API-ключ Gemini",
"geminiApiKeyPlaceholder": "Введите ваш API-ключ Gemini",
"geminiGetApiKeyText": "Получите ваш API-ключ от",
"warningMissingGeminiKey": "⚠️ API-ключ Gemini необходим для генерации изображений. Настройте его выше."
},
"RUN_SLASH_COMMAND": {
"name": "Включить слэш-команды, инициированные моделью",

View file

@ -750,7 +750,13 @@
"modelSelectionLabel": "Görüntü Üretim Modeli",
"modelSelectionDescription": "Görüntü üretimi için kullanılacak modeli seçin",
"warningMissingKey": "⚠️ Görüntü üretimi için OpenRouter API anahtarı gereklidir. Lütfen yukarıda yapılandırın.",
"successConfigured": "✓ Görüntü üretimi yapılandırılmış ve kullanıma hazır"
"successConfigured": "✓ Görüntü üretimi yapılandırılmış ve kullanıma hazır",
"providerLabel": "Görüntü üretim sağlayıcısı",
"providerDescription": "Görüntü üretimi için sağlayıcıyı seçin",
"geminiApiKeyLabel": "Gemini API Anahtarı",
"geminiApiKeyPlaceholder": "Gemini API anahtarınızı girin",
"geminiGetApiKeyText": "API anahtarınızı alın",
"warningMissingGeminiKey": "⚠️ Görüntü üretimi için Gemini API anahtarı gereklidir. Lütfen yukarıda yapılandırın."
},
"RUN_SLASH_COMMAND": {
"name": "Model tarafından başlatılan slash komutlarını etkinleştir",

View file

@ -750,7 +750,13 @@
"modelSelectionLabel": "Mô hình tạo hình ảnh",
"modelSelectionDescription": "Chọn mô hình để sử dụng cho việc tạo hình ảnh",
"warningMissingKey": "⚠️ Khóa API OpenRouter là bắt buộc để tạo hình ảnh. Vui lòng cấu hình ở trên.",
"successConfigured": "✓ Tạo hình ảnh đã được cấu hình và sẵn sàng sử dụng"
"successConfigured": "✓ Tạo hình ảnh đã được cấu hình và sẵn sàng sử dụng",
"providerLabel": "Nhà cung cấp tạo hình ảnh",
"providerDescription": "Chọn nhà cung cấp cho việc tạo hình ảnh",
"geminiApiKeyLabel": "Khóa API Gemini",
"geminiApiKeyPlaceholder": "Nhập khóa API Gemini của bạn",
"geminiGetApiKeyText": "Lấy khóa API của bạn từ",
"warningMissingGeminiKey": "⚠️ Khóa API Gemini là bắt buộc để tạo hình ảnh. Vui lòng cấu hình ở trên."
},
"RUN_SLASH_COMMAND": {
"name": "Bật lệnh slash do mô hình khởi tạo",

View file

@ -750,7 +750,13 @@
"modelSelectionLabel": "图像生成模型",
"modelSelectionDescription": "选择用于图像生成的模型",
"warningMissingKey": "⚠️ 图像生成需要 OpenRouter API 密钥。请在上方配置。",
"successConfigured": "✓ 图像生成已配置完成,可以使用"
"successConfigured": "✓ 图像生成已配置完成,可以使用",
"providerLabel": "图像生成提供商",
"providerDescription": "选择图像生成的提供商",
"geminiApiKeyLabel": "Gemini API 密钥",
"geminiApiKeyPlaceholder": "输入您的 Gemini API 密钥",
"geminiGetApiKeyText": "获取您的 API 密钥",
"warningMissingGeminiKey": "⚠️ 图像生成需要 Gemini API 密钥。请在上方配置。"
},
"RUN_SLASH_COMMAND": {
"name": "启用模型发起的斜杠命令",

View file

@ -750,7 +750,13 @@
"modelSelectionLabel": "圖像生成模型",
"modelSelectionDescription": "選擇用於圖像生成的模型",
"warningMissingKey": "⚠️ 圖像生成需要 OpenRouter API 金鑰。請在上方設定。",
"successConfigured": "✓ 圖像生成已設定完成並準備使用"
"successConfigured": "✓ 圖像生成已設定完成並準備使用",
"providerLabel": "圖像生成供應商",
"providerDescription": "選擇圖像生成的供應商",
"geminiApiKeyLabel": "Gemini API 金鑰",
"geminiApiKeyPlaceholder": "輸入您的 Gemini API 金鑰",
"geminiGetApiKeyText": "取得您的 API 金鑰從",
"warningMissingGeminiKey": "⚠️ 圖像生成需要 Gemini API 金鑰。請在上方設定。"
},
"RUN_SLASH_COMMAND": {
"name": "啟用模型啟動的斜線命令",