diff --git a/backend/internal/domain/constants.go b/backend/internal/domain/constants.go
index d7bb50fc..8a6621a1 100644
--- a/backend/internal/domain/constants.go
+++ b/backend/internal/domain/constants.go
@@ -84,10 +84,12 @@ var DefaultAntigravityModelMapping = map[string]string{
"claude-haiku-4-5": "claude-sonnet-4-5",
"claude-haiku-4-5-20251001": "claude-sonnet-4-5",
// Gemini 2.5 白名单
- "gemini-2.5-flash": "gemini-2.5-flash",
- "gemini-2.5-flash-lite": "gemini-2.5-flash-lite",
- "gemini-2.5-flash-thinking": "gemini-2.5-flash-thinking",
- "gemini-2.5-pro": "gemini-2.5-pro",
+ "gemini-2.5-flash": "gemini-2.5-flash",
+ "gemini-2.5-flash-image": "gemini-2.5-flash-image",
+ "gemini-2.5-flash-image-preview": "gemini-2.5-flash-image",
+ "gemini-2.5-flash-lite": "gemini-2.5-flash-lite",
+ "gemini-2.5-flash-thinking": "gemini-2.5-flash-thinking",
+ "gemini-2.5-pro": "gemini-2.5-pro",
// Gemini 3 白名单
"gemini-3-flash": "gemini-3-flash",
"gemini-3-pro-high": "gemini-3-pro-high",
diff --git a/backend/internal/domain/constants_test.go b/backend/internal/domain/constants_test.go
index 29605ac6..de66137f 100644
--- a/backend/internal/domain/constants_test.go
+++ b/backend/internal/domain/constants_test.go
@@ -6,6 +6,8 @@ func TestDefaultAntigravityModelMapping_ImageCompatibilityAliases(t *testing.T)
t.Parallel()
cases := map[string]string{
+ "gemini-2.5-flash-image": "gemini-2.5-flash-image",
+ "gemini-2.5-flash-image-preview": "gemini-2.5-flash-image",
"gemini-3.1-flash-image": "gemini-3.1-flash-image",
"gemini-3.1-flash-image-preview": "gemini-3.1-flash-image",
"gemini-3-pro-image": "gemini-3.1-flash-image",
diff --git a/backend/internal/handler/admin/account_handler.go b/backend/internal/handler/admin/account_handler.go
index fad8a33c..7c4d4638 100644
--- a/backend/internal/handler/admin/account_handler.go
+++ b/backend/internal/handler/admin/account_handler.go
@@ -628,6 +628,7 @@ func (h *AccountHandler) Delete(c *gin.Context) {
// TestAccountRequest represents the request body for testing an account
type TestAccountRequest struct {
ModelID string `json:"model_id"`
+ Prompt string `json:"prompt"`
}
type SyncFromCRSRequest struct {
@@ -658,7 +659,7 @@ func (h *AccountHandler) Test(c *gin.Context) {
_ = c.ShouldBindJSON(&req)
// Use AccountTestService to test the account with SSE streaming
- if err := h.accountTestService.TestAccountConnection(c, accountID, req.ModelID); err != nil {
+ if err := h.accountTestService.TestAccountConnection(c, accountID, req.ModelID, req.Prompt); err != nil {
// Error already sent via SSE, just log
return
}
diff --git a/backend/internal/pkg/antigravity/claude_types.go b/backend/internal/pkg/antigravity/claude_types.go
index 7cc68060..8ea87f18 100644
--- a/backend/internal/pkg/antigravity/claude_types.go
+++ b/backend/internal/pkg/antigravity/claude_types.go
@@ -159,6 +159,8 @@ var claudeModels = []modelDef{
// Antigravity 支持的 Gemini 模型
var geminiModels = []modelDef{
{ID: "gemini-2.5-flash", DisplayName: "Gemini 2.5 Flash", CreatedAt: "2025-01-01T00:00:00Z"},
+ {ID: "gemini-2.5-flash-image", DisplayName: "Gemini 2.5 Flash Image", CreatedAt: "2025-01-01T00:00:00Z"},
+ {ID: "gemini-2.5-flash-image-preview", DisplayName: "Gemini 2.5 Flash Image Preview", CreatedAt: "2025-01-01T00:00:00Z"},
{ID: "gemini-2.5-flash-lite", DisplayName: "Gemini 2.5 Flash Lite", CreatedAt: "2025-01-01T00:00:00Z"},
{ID: "gemini-2.5-flash-thinking", DisplayName: "Gemini 2.5 Flash Thinking", CreatedAt: "2025-01-01T00:00:00Z"},
{ID: "gemini-3-flash", DisplayName: "Gemini 3 Flash", CreatedAt: "2025-06-01T00:00:00Z"},
diff --git a/backend/internal/pkg/antigravity/claude_types_test.go b/backend/internal/pkg/antigravity/claude_types_test.go
index f7cb0a24..9fc09b1b 100644
--- a/backend/internal/pkg/antigravity/claude_types_test.go
+++ b/backend/internal/pkg/antigravity/claude_types_test.go
@@ -13,6 +13,8 @@ func TestDefaultModels_ContainsNewAndLegacyImageModels(t *testing.T) {
requiredIDs := []string{
"claude-opus-4-6-thinking",
+ "gemini-2.5-flash-image",
+ "gemini-2.5-flash-image-preview",
"gemini-3.1-flash-image",
"gemini-3.1-flash-image-preview",
"gemini-3-pro-image", // legacy compatibility
diff --git a/backend/internal/pkg/gemini/models.go b/backend/internal/pkg/gemini/models.go
index c300b17d..882d2ebd 100644
--- a/backend/internal/pkg/gemini/models.go
+++ b/backend/internal/pkg/gemini/models.go
@@ -18,10 +18,12 @@ func DefaultModels() []Model {
return []Model{
{Name: "models/gemini-2.0-flash", SupportedGenerationMethods: methods},
{Name: "models/gemini-2.5-flash", SupportedGenerationMethods: methods},
+ {Name: "models/gemini-2.5-flash-image", SupportedGenerationMethods: methods},
{Name: "models/gemini-2.5-pro", SupportedGenerationMethods: methods},
{Name: "models/gemini-3-flash-preview", SupportedGenerationMethods: methods},
{Name: "models/gemini-3-pro-preview", SupportedGenerationMethods: methods},
{Name: "models/gemini-3.1-pro-preview", SupportedGenerationMethods: methods},
+ {Name: "models/gemini-3.1-flash-image", SupportedGenerationMethods: methods},
}
}
diff --git a/backend/internal/pkg/gemini/models_test.go b/backend/internal/pkg/gemini/models_test.go
new file mode 100644
index 00000000..b80047fb
--- /dev/null
+++ b/backend/internal/pkg/gemini/models_test.go
@@ -0,0 +1,28 @@
+package gemini
+
+import "testing"
+
+func TestDefaultModels_ContainsImageModels(t *testing.T) {
+ t.Parallel()
+
+ models := DefaultModels()
+ byName := make(map[string]Model, len(models))
+ for _, model := range models {
+ byName[model.Name] = model
+ }
+
+ required := []string{
+ "models/gemini-2.5-flash-image",
+ "models/gemini-3.1-flash-image",
+ }
+
+ for _, name := range required {
+ model, ok := byName[name]
+ if !ok {
+ t.Fatalf("expected fallback model %q to exist", name)
+ }
+ if len(model.SupportedGenerationMethods) == 0 {
+ t.Fatalf("expected fallback model %q to advertise generation methods", name)
+ }
+ }
+}
diff --git a/backend/internal/pkg/geminicli/models.go b/backend/internal/pkg/geminicli/models.go
index 1fc4d983..195fb06f 100644
--- a/backend/internal/pkg/geminicli/models.go
+++ b/backend/internal/pkg/geminicli/models.go
@@ -13,10 +13,12 @@ type Model struct {
var DefaultModels = []Model{
{ID: "gemini-2.0-flash", Type: "model", DisplayName: "Gemini 2.0 Flash", CreatedAt: ""},
{ID: "gemini-2.5-flash", Type: "model", DisplayName: "Gemini 2.5 Flash", CreatedAt: ""},
+ {ID: "gemini-2.5-flash-image", Type: "model", DisplayName: "Gemini 2.5 Flash Image", CreatedAt: ""},
{ID: "gemini-2.5-pro", Type: "model", DisplayName: "Gemini 2.5 Pro", CreatedAt: ""},
{ID: "gemini-3-flash-preview", Type: "model", DisplayName: "Gemini 3 Flash Preview", CreatedAt: ""},
{ID: "gemini-3-pro-preview", Type: "model", DisplayName: "Gemini 3 Pro Preview", CreatedAt: ""},
{ID: "gemini-3.1-pro-preview", Type: "model", DisplayName: "Gemini 3.1 Pro Preview", CreatedAt: ""},
+ {ID: "gemini-3.1-flash-image", Type: "model", DisplayName: "Gemini 3.1 Flash Image", CreatedAt: ""},
}
// DefaultTestModel is the default model to preselect in test flows.
diff --git a/backend/internal/pkg/geminicli/models_test.go b/backend/internal/pkg/geminicli/models_test.go
new file mode 100644
index 00000000..c1884e2e
--- /dev/null
+++ b/backend/internal/pkg/geminicli/models_test.go
@@ -0,0 +1,23 @@
+package geminicli
+
+import "testing"
+
+func TestDefaultModels_ContainsImageModels(t *testing.T) {
+ t.Parallel()
+
+ byID := make(map[string]Model, len(DefaultModels))
+ for _, model := range DefaultModels {
+ byID[model.ID] = model
+ }
+
+ required := []string{
+ "gemini-2.5-flash-image",
+ "gemini-3.1-flash-image",
+ }
+
+ for _, id := range required {
+ if _, ok := byID[id]; !ok {
+ t.Fatalf("expected curated Gemini model %q to exist", id)
+ }
+ }
+}
diff --git a/backend/internal/service/account_test_service.go b/backend/internal/service/account_test_service.go
index b44f29fd..472551cf 100644
--- a/backend/internal/service/account_test_service.go
+++ b/backend/internal/service/account_test_service.go
@@ -45,16 +45,23 @@ const (
// TestEvent represents a SSE event for account testing
type TestEvent struct {
- Type string `json:"type"`
- Text string `json:"text,omitempty"`
- Model string `json:"model,omitempty"`
- Status string `json:"status,omitempty"`
- Code string `json:"code,omitempty"`
- Data any `json:"data,omitempty"`
- Success bool `json:"success,omitempty"`
- Error string `json:"error,omitempty"`
+ Type string `json:"type"`
+ Text string `json:"text,omitempty"`
+ Model string `json:"model,omitempty"`
+ Status string `json:"status,omitempty"`
+ Code string `json:"code,omitempty"`
+ ImageURL string `json:"image_url,omitempty"`
+ MimeType string `json:"mime_type,omitempty"`
+ Data any `json:"data,omitempty"`
+ Success bool `json:"success,omitempty"`
+ Error string `json:"error,omitempty"`
}
+const (
+ defaultGeminiTextTestPrompt = "hi"
+ defaultGeminiImageTestPrompt = "Generate a cute orange cat astronaut sticker on a clean pastel background."
+)
+
// AccountTestService handles account testing operations
type AccountTestService struct {
accountRepo AccountRepository
@@ -161,7 +168,7 @@ func createTestPayload(modelID string) (map[string]any, error) {
// TestAccountConnection tests an account's connection by sending a test request
// All account types use full Claude Code client characteristics, only auth header differs
// modelID is optional - if empty, defaults to claude.DefaultTestModel
-func (s *AccountTestService) TestAccountConnection(c *gin.Context, accountID int64, modelID string) error {
+func (s *AccountTestService) TestAccountConnection(c *gin.Context, accountID int64, modelID string, prompt string) error {
ctx := c.Request.Context()
// Get account
@@ -176,11 +183,11 @@ func (s *AccountTestService) TestAccountConnection(c *gin.Context, accountID int
}
if account.IsGemini() {
- return s.testGeminiAccountConnection(c, account, modelID)
+ return s.testGeminiAccountConnection(c, account, modelID, prompt)
}
if account.Platform == PlatformAntigravity {
- return s.routeAntigravityTest(c, account, modelID)
+ return s.routeAntigravityTest(c, account, modelID, prompt)
}
if account.Platform == PlatformSora {
@@ -435,7 +442,7 @@ func (s *AccountTestService) testOpenAIAccountConnection(c *gin.Context, account
}
// testGeminiAccountConnection tests a Gemini account's connection
-func (s *AccountTestService) testGeminiAccountConnection(c *gin.Context, account *Account, modelID string) error {
+func (s *AccountTestService) testGeminiAccountConnection(c *gin.Context, account *Account, modelID string, prompt string) error {
ctx := c.Request.Context()
// Determine the model to use
@@ -462,7 +469,7 @@ func (s *AccountTestService) testGeminiAccountConnection(c *gin.Context, account
c.Writer.Flush()
// Create test payload (Gemini format)
- payload := createGeminiTestPayload()
+ payload := createGeminiTestPayload(testModelID, prompt)
// Build request based on account type
var req *http.Request
@@ -1198,10 +1205,10 @@ func truncateSoraErrorBody(body []byte, max int) string {
// routeAntigravityTest 路由 Antigravity 账号的测试请求。
// APIKey 类型走原生协议(与 gateway_handler 路由一致),OAuth/Upstream 走 CRS 中转。
-func (s *AccountTestService) routeAntigravityTest(c *gin.Context, account *Account, modelID string) error {
+func (s *AccountTestService) routeAntigravityTest(c *gin.Context, account *Account, modelID string, prompt string) error {
if account.Type == AccountTypeAPIKey {
if strings.HasPrefix(modelID, "gemini-") {
- return s.testGeminiAccountConnection(c, account, modelID)
+ return s.testGeminiAccountConnection(c, account, modelID, prompt)
}
return s.testClaudeAccountConnection(c, account, modelID)
}
@@ -1349,14 +1356,46 @@ func (s *AccountTestService) buildCodeAssistRequest(ctx context.Context, accessT
return req, nil
}
-// createGeminiTestPayload creates a minimal test payload for Gemini API
-func createGeminiTestPayload() []byte {
+// createGeminiTestPayload creates a minimal test payload for Gemini API.
+// Image models use the image-generation path so the frontend can preview the returned image.
+func createGeminiTestPayload(modelID string, prompt string) []byte {
+ if isImageGenerationModel(modelID) {
+ imagePrompt := strings.TrimSpace(prompt)
+ if imagePrompt == "" {
+ imagePrompt = defaultGeminiImageTestPrompt
+ }
+
+ payload := map[string]any{
+ "contents": []map[string]any{
+ {
+ "role": "user",
+ "parts": []map[string]any{
+ {"text": imagePrompt},
+ },
+ },
+ },
+ "generationConfig": map[string]any{
+ "responseModalities": []string{"TEXT", "IMAGE"},
+ "imageConfig": map[string]any{
+ "aspectRatio": "1:1",
+ },
+ },
+ }
+ bytes, _ := json.Marshal(payload)
+ return bytes
+ }
+
+ textPrompt := strings.TrimSpace(prompt)
+ if textPrompt == "" {
+ textPrompt = defaultGeminiTextTestPrompt
+ }
+
payload := map[string]any{
"contents": []map[string]any{
{
"role": "user",
"parts": []map[string]any{
- {"text": "hi"},
+ {"text": textPrompt},
},
},
},
@@ -1416,6 +1455,17 @@ func (s *AccountTestService) processGeminiStream(c *gin.Context, body io.Reader)
if text, ok := partMap["text"].(string); ok && text != "" {
s.sendEvent(c, TestEvent{Type: "content", Text: text})
}
+ if inlineData, ok := partMap["inlineData"].(map[string]any); ok {
+ mimeType, _ := inlineData["mimeType"].(string)
+ data, _ := inlineData["data"].(string)
+ if strings.HasPrefix(strings.ToLower(mimeType), "image/") && data != "" {
+ s.sendEvent(c, TestEvent{
+ Type: "image",
+ ImageURL: fmt.Sprintf("data:%s;base64,%s", mimeType, data),
+ MimeType: mimeType,
+ })
+ }
+ }
}
}
}
@@ -1602,7 +1652,7 @@ func (s *AccountTestService) RunTestBackground(ctx context.Context, accountID in
ginCtx, _ := gin.CreateTestContext(w)
ginCtx.Request = (&http.Request{}).WithContext(ctx)
- testErr := s.TestAccountConnection(ginCtx, accountID, modelID)
+ testErr := s.TestAccountConnection(ginCtx, accountID, modelID, "")
finishedAt := time.Now()
body := w.Body.String()
diff --git a/backend/internal/service/account_test_service_gemini_test.go b/backend/internal/service/account_test_service_gemini_test.go
new file mode 100644
index 00000000..5ba04c69
--- /dev/null
+++ b/backend/internal/service/account_test_service_gemini_test.go
@@ -0,0 +1,59 @@
+//go:build unit
+
+package service
+
+import (
+ "encoding/json"
+ "strings"
+ "testing"
+
+ "github.com/gin-gonic/gin"
+ "github.com/stretchr/testify/require"
+)
+
+func TestCreateGeminiTestPayload_ImageModel(t *testing.T) {
+ t.Parallel()
+
+ payload := createGeminiTestPayload("gemini-2.5-flash-image", "draw a tiny robot")
+
+ var parsed struct {
+ Contents []struct {
+ Parts []struct {
+ Text string `json:"text"`
+ } `json:"parts"`
+ } `json:"contents"`
+ GenerationConfig struct {
+ ResponseModalities []string `json:"responseModalities"`
+ ImageConfig struct {
+ AspectRatio string `json:"aspectRatio"`
+ } `json:"imageConfig"`
+ } `json:"generationConfig"`
+ }
+
+ require.NoError(t, json.Unmarshal(payload, &parsed))
+ require.Len(t, parsed.Contents, 1)
+ require.Len(t, parsed.Contents[0].Parts, 1)
+ require.Equal(t, "draw a tiny robot", parsed.Contents[0].Parts[0].Text)
+ require.Equal(t, []string{"TEXT", "IMAGE"}, parsed.GenerationConfig.ResponseModalities)
+ require.Equal(t, "1:1", parsed.GenerationConfig.ImageConfig.AspectRatio)
+}
+
+func TestProcessGeminiStream_EmitsImageEvent(t *testing.T) {
+ t.Parallel()
+ gin.SetMode(gin.TestMode)
+
+ ctx, recorder := newSoraTestContext()
+ svc := &AccountTestService{}
+
+ stream := strings.NewReader("data: {\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"ok\"},{\"inlineData\":{\"mimeType\":\"image/png\",\"data\":\"QUJD\"}}]}}]}\n\ndata: [DONE]\n\n")
+
+ err := svc.processGeminiStream(ctx, stream)
+ require.NoError(t, err)
+
+ body := recorder.Body.String()
+ require.Contains(t, body, "\"type\":\"content\"")
+ require.Contains(t, body, "\"text\":\"ok\"")
+ require.Contains(t, body, "\"type\":\"image\"")
+ require.Contains(t, body, "\"image_url\":\"data:image/png;base64,QUJD\"")
+ require.Contains(t, body, "\"mime_type\":\"image/png\"")
+}
diff --git a/backend/migrations/071_add_gemini25_flash_image_to_model_mapping.sql b/backend/migrations/071_add_gemini25_flash_image_to_model_mapping.sql
new file mode 100644
index 00000000..f3cb3d37
--- /dev/null
+++ b/backend/migrations/071_add_gemini25_flash_image_to_model_mapping.sql
@@ -0,0 +1,51 @@
+-- Add gemini-2.5-flash-image aliases to Antigravity model_mapping
+--
+-- Background:
+-- Gemini native image generation now relies on gemini-2.5-flash-image, and
+-- existing Antigravity accounts with persisted model_mapping need this alias in
+-- order to participate in mixed scheduling from gemini groups.
+--
+-- Strategy:
+-- Overwrite the stored model_mapping so it matches DefaultAntigravityModelMapping
+-- in constants.go, including legacy gemini-3-pro-image aliases.
+
+UPDATE accounts
+SET credentials = jsonb_set(
+ credentials,
+ '{model_mapping}',
+ '{
+ "claude-opus-4-6-thinking": "claude-opus-4-6-thinking",
+ "claude-opus-4-6": "claude-opus-4-6-thinking",
+ "claude-opus-4-5-thinking": "claude-opus-4-6-thinking",
+ "claude-opus-4-5-20251101": "claude-opus-4-6-thinking",
+ "claude-sonnet-4-6": "claude-sonnet-4-6",
+ "claude-sonnet-4-5": "claude-sonnet-4-5",
+ "claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking",
+ "claude-sonnet-4-5-20250929": "claude-sonnet-4-5",
+ "claude-haiku-4-5": "claude-sonnet-4-5",
+ "claude-haiku-4-5-20251001": "claude-sonnet-4-5",
+ "gemini-2.5-flash": "gemini-2.5-flash",
+ "gemini-2.5-flash-image": "gemini-2.5-flash-image",
+ "gemini-2.5-flash-image-preview": "gemini-2.5-flash-image",
+ "gemini-2.5-flash-lite": "gemini-2.5-flash-lite",
+ "gemini-2.5-flash-thinking": "gemini-2.5-flash-thinking",
+ "gemini-2.5-pro": "gemini-2.5-pro",
+ "gemini-3-flash": "gemini-3-flash",
+ "gemini-3-pro-high": "gemini-3-pro-high",
+ "gemini-3-pro-low": "gemini-3-pro-low",
+ "gemini-3-flash-preview": "gemini-3-flash",
+ "gemini-3-pro-preview": "gemini-3-pro-high",
+ "gemini-3.1-pro-high": "gemini-3.1-pro-high",
+ "gemini-3.1-pro-low": "gemini-3.1-pro-low",
+ "gemini-3.1-pro-preview": "gemini-3.1-pro-high",
+ "gemini-3.1-flash-image": "gemini-3.1-flash-image",
+ "gemini-3.1-flash-image-preview": "gemini-3.1-flash-image",
+ "gemini-3-pro-image": "gemini-3.1-flash-image",
+ "gemini-3-pro-image-preview": "gemini-3.1-flash-image",
+ "gpt-oss-120b-medium": "gpt-oss-120b-medium",
+ "tab_flash_lite_preview": "tab_flash_lite_preview"
+ }'::jsonb
+)
+WHERE platform = 'antigravity'
+ AND deleted_at IS NULL
+ AND credentials->'model_mapping' IS NOT NULL;
diff --git a/frontend/src/components/account/AccountStatusIndicator.vue b/frontend/src/components/account/AccountStatusIndicator.vue
index 1dc4f287..220b5c8b 100644
--- a/frontend/src/components/account/AccountStatusIndicator.vue
+++ b/frontend/src/components/account/AccountStatusIndicator.vue
@@ -176,6 +176,7 @@ const formatScopeName = (scope: string): string => {
'gemini-2.5-flash-lite': 'G25FL',
'gemini-2.5-flash-thinking': 'G25FT',
'gemini-2.5-pro': 'G25P',
+ 'gemini-2.5-flash-image': 'G25I',
// Gemini 3 系列
'gemini-3-flash': 'G3F',
'gemini-3.1-pro-high': 'G3PH',
diff --git a/frontend/src/components/account/AccountTestModal.vue b/frontend/src/components/account/AccountTestModal.vue
index 792a8f45..e731a7b1 100644
--- a/frontend/src/components/account/AccountTestModal.vue
+++ b/frontend/src/components/account/AccountTestModal.vue
@@ -15,7 +15,7 @@
-
+
{{ account.name }}
@@ -61,6 +61,17 @@
{{ t('admin.accounts.soraTestHint') }}
+
+
+
+
-
+
{{ t('admin.accounts.readyToTest') }}
-
+
{{ t('admin.accounts.connectingToApi') }}
@@ -106,21 +103,14 @@
v-if="status === 'success'"
class="mt-3 flex items-center gap-2 border-t border-gray-700 pt-3 text-green-400"
>
-
+
{{ t('admin.accounts.testCompleted') }}
-
+
{{ errorMessage }}
@@ -132,21 +122,48 @@
class="absolute right-2 top-2 rounded-lg bg-gray-800/80 p-1.5 text-gray-400 opacity-0 transition-all hover:bg-gray-700 hover:text-white group-hover:opacity-100"
:title="t('admin.accounts.copyOutput')"
>
-
+
+
+
+ {{ t('admin.accounts.geminiImagePreview') }}
+
+
+
+
-
+
{{ isSoraAccount ? t('admin.accounts.soraTestTarget') : t('admin.accounts.testModel') }}
-
- {{ isSoraAccount ? t('admin.accounts.soraTestMode') : t('admin.accounts.testPrompt') }}
+
+ {{
+ isSoraAccount
+ ? t('admin.accounts.soraTestMode')
+ : supportsGeminiImageTest
+ ? t('admin.accounts.geminiImageTestMode')
+ : t('admin.accounts.testPrompt')
+ }}
@@ -174,54 +191,15 @@
: 'bg-primary-500 text-white hover:bg-primary-600'
]"
>
-
-
-
+ name="refresh"
+ size="sm"
+ class="animate-spin"
+ :stroke-width="2"
+ />
+
+
{{
status === 'connecting'
@@ -242,7 +220,8 @@ import { computed, ref, watch, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseDialog from '@/components/common/BaseDialog.vue'
import Select from '@/components/common/Select.vue'
-import Icon from '@/components/icons/Icon.vue'
+import TextArea from '@/components/common/TextArea.vue'
+import { Icon } from '@/components/icons'
import { useClipboard } from '@/composables/useClipboard'
import { adminAPI } from '@/api/admin'
import type { Account, ClaudeModel } from '@/types'
@@ -255,6 +234,11 @@ interface OutputLine {
class: string
}
+interface PreviewImage {
+ url: string
+ mimeType?: string
+}
+
const props = defineProps<{
show: boolean
account: Account | null
@@ -271,15 +255,37 @@ const streamingContent = ref('')
const errorMessage = ref('')
const availableModels = ref([])
const selectedModelId = ref('')
+const testPrompt = ref('')
const loadingModels = ref(false)
let eventSource: EventSource | null = null
const isSoraAccount = computed(() => props.account?.platform === 'sora')
+const generatedImages = ref([])
+const prioritizedGeminiModels = ['gemini-3.1-flash-image', 'gemini-2.5-flash-image', 'gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-3-flash-preview', 'gemini-3-pro-preview', 'gemini-2.0-flash']
+const supportsGeminiImageTest = computed(() => {
+ if (isSoraAccount.value) return false
+ const modelID = selectedModelId.value.toLowerCase()
+ if (!modelID.startsWith('gemini-') || !modelID.includes('-image')) return false
+
+ return props.account?.platform === 'gemini' || (props.account?.platform === 'antigravity' && props.account?.type === 'apikey')
+})
+
+const sortTestModels = (models: ClaudeModel[]) => {
+ const priorityMap = new Map(prioritizedGeminiModels.map((id, index) => [id, index]))
+
+ return [...models].sort((a, b) => {
+ const aPriority = priorityMap.get(a.id) ?? Number.MAX_SAFE_INTEGER
+ const bPriority = priorityMap.get(b.id) ?? Number.MAX_SAFE_INTEGER
+ if (aPriority !== bPriority) return aPriority - bPriority
+ return 0
+ })
+}
// Load available models when modal opens
watch(
() => props.show,
async (newVal) => {
if (newVal && props.account) {
+ testPrompt.value = ''
resetState()
await loadAvailableModels()
} else {
@@ -288,6 +294,12 @@ watch(
}
)
+watch(selectedModelId, () => {
+ if (supportsGeminiImageTest.value && !testPrompt.value.trim()) {
+ testPrompt.value = t('admin.accounts.geminiImagePromptDefault')
+ }
+})
+
const loadAvailableModels = async () => {
if (!props.account) return
if (props.account.platform === 'sora') {
@@ -300,17 +312,14 @@ const loadAvailableModels = async () => {
loadingModels.value = true
selectedModelId.value = '' // Reset selection before loading
try {
- availableModels.value = await adminAPI.accounts.getAvailableModels(props.account.id)
+ const models = await adminAPI.accounts.getAvailableModels(props.account.id)
+ availableModels.value = props.account.platform === 'gemini' || props.account.platform === 'antigravity'
+ ? sortTestModels(models)
+ : models
// Default selection by platform
if (availableModels.value.length > 0) {
if (props.account.platform === 'gemini') {
- const preferred =
- availableModels.value.find((m) => m.id === 'gemini-2.0-flash') ||
- availableModels.value.find((m) => m.id === 'gemini-2.5-flash') ||
- availableModels.value.find((m) => m.id === 'gemini-2.5-pro') ||
- availableModels.value.find((m) => m.id === 'gemini-3-flash-preview') ||
- availableModels.value.find((m) => m.id === 'gemini-3-pro-preview')
- selectedModelId.value = preferred?.id || availableModels.value[0].id
+ selectedModelId.value = availableModels.value[0].id
} else {
// Try to select Sonnet as default, otherwise use first model
const sonnetModel = availableModels.value.find((m) => m.id.includes('sonnet'))
@@ -332,6 +341,7 @@ const resetState = () => {
outputLines.value = []
streamingContent.value = ''
errorMessage.value = ''
+ generatedImages.value = []
}
const handleClose = () => {
@@ -385,7 +395,12 @@ const startTest = async () => {
'Content-Type': 'application/json'
},
body: JSON.stringify(
- isSoraAccount.value ? {} : { model_id: selectedModelId.value }
+ isSoraAccount.value
+ ? {}
+ : {
+ model_id: selectedModelId.value,
+ prompt: supportsGeminiImageTest.value ? testPrompt.value.trim() : ''
+ }
)
})
@@ -436,6 +451,8 @@ const handleEvent = (event: {
model?: string
success?: boolean
error?: string
+ image_url?: string
+ mime_type?: string
}) => {
switch (event.type) {
case 'test_start':
@@ -444,7 +461,11 @@ const handleEvent = (event: {
addLine(t('admin.accounts.usingModel', { model: event.model }), 'text-cyan-400')
}
addLine(
- isSoraAccount.value ? t('admin.accounts.soraTestingFlow') : t('admin.accounts.sendingTestMessage'),
+ isSoraAccount.value
+ ? t('admin.accounts.soraTestingFlow')
+ : supportsGeminiImageTest.value
+ ? t('admin.accounts.sendingGeminiImageRequest')
+ : t('admin.accounts.sendingTestMessage'),
'text-gray-400'
)
addLine('', 'text-gray-300')
@@ -458,6 +479,16 @@ const handleEvent = (event: {
}
break
+ case 'image':
+ if (event.image_url) {
+ generatedImages.value.push({
+ url: event.image_url,
+ mimeType: event.mime_type
+ })
+ addLine(t('admin.accounts.geminiImageReceived', { count: generatedImages.value.length }), 'text-purple-300')
+ }
+ break
+
case 'test_complete':
// Move streaming content to output lines
if (streamingContent.value) {
diff --git a/frontend/src/components/account/AccountUsageCell.vue b/frontend/src/components/account/AccountUsageCell.vue
index f5cab570..e83eaead 100644
--- a/frontend/src/components/account/AccountUsageCell.vue
+++ b/frontend/src/components/account/AccountUsageCell.vue
@@ -521,7 +521,7 @@ const antigravity3FlashUsageFromAPI = computed(() => getAntigravityUsageFromAPI(
// Gemini Image from API
const antigravity3ImageUsageFromAPI = computed(() =>
- getAntigravityUsageFromAPI(['gemini-3.1-flash-image', 'gemini-3-pro-image'])
+ getAntigravityUsageFromAPI(['gemini-2.5-flash-image', 'gemini-3.1-flash-image', 'gemini-3-pro-image'])
)
// Claude from API (all Claude model variants)
diff --git a/frontend/src/components/account/BulkEditAccountModal.vue b/frontend/src/components/account/BulkEditAccountModal.vue
index 1d6f32fe..c6e08684 100644
--- a/frontend/src/components/account/BulkEditAccountModal.vue
+++ b/frontend/src/components/account/BulkEditAccountModal.vue
@@ -959,10 +959,11 @@ const allModels = [
{ value: 'gpt-5.1-2025-11-13', label: 'GPT-5.1' },
{ value: 'gpt-5.1-codex-mini', label: 'GPT-5.1 Codex Mini' },
{ value: 'gpt-5-2025-08-07', label: 'GPT-5' },
+ { value: 'gemini-3.1-flash-image', label: 'Gemini 3.1 Flash Image' },
+ { value: 'gemini-2.5-flash-image', label: 'Gemini 2.5 Flash Image' },
{ value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash' },
{ value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' },
{ value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' },
- { value: 'gemini-3.1-flash-image', label: 'Gemini 3.1 Flash Image' },
{ value: 'gemini-3-pro-image', label: 'Gemini 3 Pro Image (Legacy)' },
{ value: 'gemini-3-flash-preview', label: 'Gemini 3 Flash Preview' },
{ value: 'gemini-3-pro-preview', label: 'Gemini 3 Pro Preview' }
@@ -1042,6 +1043,12 @@ const presetMappings = [
to: 'claude-sonnet-4-5-20250929',
color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400'
},
+ {
+ label: 'Gemini 2.5 Image',
+ from: 'gemini-2.5-flash-image',
+ to: 'gemini-2.5-flash-image',
+ color: 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-400'
+ },
{
label: 'Gemini 3.1 Image',
from: 'gemini-3.1-flash-image',
diff --git a/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts b/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts
index 2681f0cb..7cccbf63 100644
--- a/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts
+++ b/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts
@@ -32,6 +32,10 @@ describe('AccountUsageCell', () => {
it('Antigravity 图片用量会聚合新旧 image 模型', async () => {
getUsage.mockResolvedValue({
antigravity_quota: {
+ 'gemini-2.5-flash-image': {
+ utilization: 45,
+ reset_time: '2026-03-01T11:00:00Z'
+ },
'gemini-3.1-flash-image': {
utilization: 20,
reset_time: '2026-03-01T10:00:00Z'
diff --git a/frontend/src/components/account/__tests__/BulkEditAccountModal.spec.ts b/frontend/src/components/account/__tests__/BulkEditAccountModal.spec.ts
index 28ac61ec..ba3422ca 100644
--- a/frontend/src/components/account/__tests__/BulkEditAccountModal.spec.ts
+++ b/frontend/src/components/account/__tests__/BulkEditAccountModal.spec.ts
@@ -18,6 +18,10 @@ vi.mock('@/api/admin', () => ({
}
}))
+vi.mock('@/api/admin/accounts', () => ({
+ getAntigravityDefaultModelMapping: vi.fn()
+}))
+
vi.mock('vue-i18n', async () => {
const actual = await vi.importActual('vue-i18n')
return {
diff --git a/frontend/src/components/admin/account/AccountTestModal.vue b/frontend/src/components/admin/account/AccountTestModal.vue
index a25c25cc..e731a7b1 100644
--- a/frontend/src/components/admin/account/AccountTestModal.vue
+++ b/frontend/src/components/admin/account/AccountTestModal.vue
@@ -61,6 +61,17 @@
{{ t('admin.accounts.soraTestHint') }}
+
+
+
+
+
+
+ {{ t('admin.accounts.geminiImagePreview') }}
+
+
+
+
@@ -125,7 +157,13 @@
- {{ isSoraAccount ? t('admin.accounts.soraTestMode') : t('admin.accounts.testPrompt') }}
+ {{
+ isSoraAccount
+ ? t('admin.accounts.soraTestMode')
+ : supportsGeminiImageTest
+ ? t('admin.accounts.geminiImageTestMode')
+ : t('admin.accounts.testPrompt')
+ }}
@@ -182,6 +220,7 @@ import { computed, ref, watch, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseDialog from '@/components/common/BaseDialog.vue'
import Select from '@/components/common/Select.vue'
+import TextArea from '@/components/common/TextArea.vue'
import { Icon } from '@/components/icons'
import { useClipboard } from '@/composables/useClipboard'
import { adminAPI } from '@/api/admin'
@@ -195,6 +234,11 @@ interface OutputLine {
class: string
}
+interface PreviewImage {
+ url: string
+ mimeType?: string
+}
+
const props = defineProps<{
show: boolean
account: Account | null
@@ -211,15 +255,37 @@ const streamingContent = ref('')
const errorMessage = ref('')
const availableModels = ref([])
const selectedModelId = ref('')
+const testPrompt = ref('')
const loadingModels = ref(false)
let eventSource: EventSource | null = null
const isSoraAccount = computed(() => props.account?.platform === 'sora')
+const generatedImages = ref([])
+const prioritizedGeminiModels = ['gemini-3.1-flash-image', 'gemini-2.5-flash-image', 'gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-3-flash-preview', 'gemini-3-pro-preview', 'gemini-2.0-flash']
+const supportsGeminiImageTest = computed(() => {
+ if (isSoraAccount.value) return false
+ const modelID = selectedModelId.value.toLowerCase()
+ if (!modelID.startsWith('gemini-') || !modelID.includes('-image')) return false
+
+ return props.account?.platform === 'gemini' || (props.account?.platform === 'antigravity' && props.account?.type === 'apikey')
+})
+
+const sortTestModels = (models: ClaudeModel[]) => {
+ const priorityMap = new Map(prioritizedGeminiModels.map((id, index) => [id, index]))
+
+ return [...models].sort((a, b) => {
+ const aPriority = priorityMap.get(a.id) ?? Number.MAX_SAFE_INTEGER
+ const bPriority = priorityMap.get(b.id) ?? Number.MAX_SAFE_INTEGER
+ if (aPriority !== bPriority) return aPriority - bPriority
+ return 0
+ })
+}
// Load available models when modal opens
watch(
() => props.show,
async (newVal) => {
if (newVal && props.account) {
+ testPrompt.value = ''
resetState()
await loadAvailableModels()
} else {
@@ -228,6 +294,12 @@ watch(
}
)
+watch(selectedModelId, () => {
+ if (supportsGeminiImageTest.value && !testPrompt.value.trim()) {
+ testPrompt.value = t('admin.accounts.geminiImagePromptDefault')
+ }
+})
+
const loadAvailableModels = async () => {
if (!props.account) return
if (props.account.platform === 'sora') {
@@ -240,17 +312,14 @@ const loadAvailableModels = async () => {
loadingModels.value = true
selectedModelId.value = '' // Reset selection before loading
try {
- availableModels.value = await adminAPI.accounts.getAvailableModels(props.account.id)
+ const models = await adminAPI.accounts.getAvailableModels(props.account.id)
+ availableModels.value = props.account.platform === 'gemini' || props.account.platform === 'antigravity'
+ ? sortTestModels(models)
+ : models
// Default selection by platform
if (availableModels.value.length > 0) {
if (props.account.platform === 'gemini') {
- const preferred =
- availableModels.value.find((m) => m.id === 'gemini-2.0-flash') ||
- availableModels.value.find((m) => m.id === 'gemini-2.5-flash') ||
- availableModels.value.find((m) => m.id === 'gemini-2.5-pro') ||
- availableModels.value.find((m) => m.id === 'gemini-3-flash-preview') ||
- availableModels.value.find((m) => m.id === 'gemini-3-pro-preview')
- selectedModelId.value = preferred?.id || availableModels.value[0].id
+ selectedModelId.value = availableModels.value[0].id
} else {
// Try to select Sonnet as default, otherwise use first model
const sonnetModel = availableModels.value.find((m) => m.id.includes('sonnet'))
@@ -272,6 +341,7 @@ const resetState = () => {
outputLines.value = []
streamingContent.value = ''
errorMessage.value = ''
+ generatedImages.value = []
}
const handleClose = () => {
@@ -325,7 +395,12 @@ const startTest = async () => {
'Content-Type': 'application/json'
},
body: JSON.stringify(
- isSoraAccount.value ? {} : { model_id: selectedModelId.value }
+ isSoraAccount.value
+ ? {}
+ : {
+ model_id: selectedModelId.value,
+ prompt: supportsGeminiImageTest.value ? testPrompt.value.trim() : ''
+ }
)
})
@@ -376,6 +451,8 @@ const handleEvent = (event: {
model?: string
success?: boolean
error?: string
+ image_url?: string
+ mime_type?: string
}) => {
switch (event.type) {
case 'test_start':
@@ -384,7 +461,11 @@ const handleEvent = (event: {
addLine(t('admin.accounts.usingModel', { model: event.model }), 'text-cyan-400')
}
addLine(
- isSoraAccount.value ? t('admin.accounts.soraTestingFlow') : t('admin.accounts.sendingTestMessage'),
+ isSoraAccount.value
+ ? t('admin.accounts.soraTestingFlow')
+ : supportsGeminiImageTest.value
+ ? t('admin.accounts.sendingGeminiImageRequest')
+ : t('admin.accounts.sendingTestMessage'),
'text-gray-400'
)
addLine('', 'text-gray-300')
@@ -398,6 +479,16 @@ const handleEvent = (event: {
}
break
+ case 'image':
+ if (event.image_url) {
+ generatedImages.value.push({
+ url: event.image_url,
+ mimeType: event.mime_type
+ })
+ addLine(t('admin.accounts.geminiImageReceived', { count: generatedImages.value.length }), 'text-purple-300')
+ }
+ break
+
case 'test_complete':
// Move streaming content to output lines
if (streamingContent.value) {
diff --git a/frontend/src/components/admin/account/__tests__/AccountTestModal.spec.ts b/frontend/src/components/admin/account/__tests__/AccountTestModal.spec.ts
new file mode 100644
index 00000000..429a905c
--- /dev/null
+++ b/frontend/src/components/admin/account/__tests__/AccountTestModal.spec.ts
@@ -0,0 +1,147 @@
+import { flushPromises, mount } from '@vue/test-utils'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import AccountTestModal from '../AccountTestModal.vue'
+
+const { getAvailableModels, copyToClipboard } = vi.hoisted(() => ({
+ getAvailableModels: vi.fn(),
+ copyToClipboard: vi.fn()
+}))
+
+vi.mock('@/api/admin', () => ({
+ adminAPI: {
+ accounts: {
+ getAvailableModels
+ }
+ }
+}))
+
+vi.mock('@/composables/useClipboard', () => ({
+ useClipboard: () => ({
+ copyToClipboard
+ })
+}))
+
+vi.mock('vue-i18n', async () => {
+ const actual = await vi.importActual('vue-i18n')
+ const messages: Record = {
+ 'admin.accounts.geminiImagePromptDefault': 'Generate a cute orange cat astronaut sticker on a clean pastel background.'
+ }
+ return {
+ ...actual,
+ useI18n: () => ({
+ t: (key: string, params?: Record) => {
+ if (key === 'admin.accounts.geminiImageReceived' && params?.count) {
+ return `received-${params.count}`
+ }
+ return messages[key] || key
+ }
+ })
+ }
+})
+
+function createStreamResponse(lines: string[]) {
+ const encoder = new TextEncoder()
+ const chunks = lines.map((line) => encoder.encode(line))
+ let index = 0
+
+ return {
+ ok: true,
+ body: {
+ getReader: () => ({
+ read: vi.fn().mockImplementation(async () => {
+ if (index < chunks.length) {
+ return { done: false, value: chunks[index++] }
+ }
+ return { done: true, value: undefined }
+ })
+ })
+ }
+ } as Response
+}
+
+function mountModal() {
+ return mount(AccountTestModal, {
+ props: {
+ show: false,
+ account: {
+ id: 42,
+ name: 'Gemini Image Test',
+ platform: 'gemini',
+ type: 'apikey',
+ status: 'active'
+ }
+ } as any,
+ global: {
+ stubs: {
+ BaseDialog: { template: '
' },
+ Select: { template: '' },
+ TextArea: {
+ props: ['modelValue'],
+ emits: ['update:modelValue'],
+ template: ''
+ },
+ Icon: true
+ }
+ }
+ })
+}
+
+describe('AccountTestModal', () => {
+ beforeEach(() => {
+ getAvailableModels.mockResolvedValue([
+ { id: 'gemini-2.0-flash', display_name: 'Gemini 2.0 Flash' },
+ { id: 'gemini-2.5-flash-image', display_name: 'Gemini 2.5 Flash Image' },
+ { id: 'gemini-3.1-flash-image', display_name: 'Gemini 3.1 Flash Image' }
+ ])
+ copyToClipboard.mockReset()
+ Object.defineProperty(globalThis, 'localStorage', {
+ value: {
+ getItem: vi.fn((key: string) => (key === 'auth_token' ? 'test-token' : null)),
+ setItem: vi.fn(),
+ removeItem: vi.fn(),
+ clear: vi.fn()
+ },
+ configurable: true
+ })
+ global.fetch = vi.fn().mockResolvedValue(
+ createStreamResponse([
+ 'data: {"type":"test_start","model":"gemini-2.5-flash-image"}\n',
+ 'data: {"type":"image","image_url":"data:image/png;base64,QUJD","mime_type":"image/png"}\n',
+ 'data: {"type":"test_complete","success":true}\n'
+ ])
+ ) as any
+ })
+
+ afterEach(() => {
+ vi.restoreAllMocks()
+ })
+
+ it('gemini 图片模型测试会携带提示词并渲染图片预览', async () => {
+ const wrapper = mountModal()
+ await wrapper.setProps({ show: true })
+ await flushPromises()
+
+ const promptInput = wrapper.find('textarea.textarea-stub')
+ expect(promptInput.exists()).toBe(true)
+ await promptInput.setValue('draw a tiny orange cat astronaut')
+
+ const buttons = wrapper.findAll('button')
+ const startButton = buttons.find((button) => button.text().includes('admin.accounts.startTest'))
+ expect(startButton).toBeTruthy()
+
+ await startButton!.trigger('click')
+ await flushPromises()
+ await flushPromises()
+
+ expect(global.fetch).toHaveBeenCalledTimes(1)
+ const [, request] = (global.fetch as any).mock.calls[0]
+ expect(JSON.parse(request.body)).toEqual({
+ model_id: 'gemini-3.1-flash-image',
+ prompt: 'draw a tiny orange cat astronaut'
+ })
+
+ const preview = wrapper.find('img[alt="gemini-test-image-1"]')
+ expect(preview.exists()).toBe(true)
+ expect(preview.attributes('src')).toBe('data:image/png;base64,QUJD')
+ })
+})
diff --git a/frontend/src/components/keys/UseKeyModal.vue b/frontend/src/components/keys/UseKeyModal.vue
index 8d59bc5e..b478c50a 100644
--- a/frontend/src/components/keys/UseKeyModal.vue
+++ b/frontend/src/components/keys/UseKeyModal.vue
@@ -959,6 +959,23 @@ function generateOpenCodeConfig(platform: string, baseUrl: string, apiKey: strin
}
}
},
+ 'gemini-2.5-flash-image': {
+ name: 'Gemini 2.5 Flash Image',
+ limit: {
+ context: 1048576,
+ output: 65536
+ },
+ modalities: {
+ input: ['text', 'image'],
+ output: ['image']
+ },
+ options: {
+ thinking: {
+ budgetTokens: 24576,
+ type: 'enabled'
+ }
+ }
+ },
'gemini-3.1-flash-image': {
name: 'Gemini 3.1 Flash Image',
limit: {
diff --git a/frontend/src/composables/__tests__/useModelWhitelist.spec.ts b/frontend/src/composables/__tests__/useModelWhitelist.spec.ts
index 79c88a29..b4308a63 100644
--- a/frontend/src/composables/__tests__/useModelWhitelist.spec.ts
+++ b/frontend/src/composables/__tests__/useModelWhitelist.spec.ts
@@ -1,4 +1,9 @@
-import { describe, expect, it } from 'vitest'
+import { describe, expect, it, vi } from 'vitest'
+
+vi.mock('@/api/admin/accounts', () => ({
+ getAntigravityDefaultModelMapping: vi.fn()
+}))
+
import { buildModelMappingObject, getModelsByPlatform } from '../useModelWhitelist'
describe('useModelWhitelist', () => {
@@ -12,10 +17,27 @@ describe('useModelWhitelist', () => {
it('antigravity 模型列表包含图片模型兼容项', () => {
const models = getModelsByPlatform('antigravity')
+ expect(models).toContain('gemini-2.5-flash-image')
expect(models).toContain('gemini-3.1-flash-image')
expect(models).toContain('gemini-3-pro-image')
})
+ it('gemini 模型列表包含原生生图模型', () => {
+ const models = getModelsByPlatform('gemini')
+
+ expect(models).toContain('gemini-2.5-flash-image')
+ expect(models).toContain('gemini-3.1-flash-image')
+ expect(models.indexOf('gemini-3.1-flash-image')).toBeLessThan(models.indexOf('gemini-2.0-flash'))
+ expect(models.indexOf('gemini-2.5-flash-image')).toBeLessThan(models.indexOf('gemini-2.5-flash'))
+ })
+
+ it('antigravity 模型列表会把新的 Gemini 图片模型排在前面', () => {
+ const models = getModelsByPlatform('antigravity')
+
+ expect(models.indexOf('gemini-3.1-flash-image')).toBeLessThan(models.indexOf('gemini-2.5-flash'))
+ expect(models.indexOf('gemini-2.5-flash-image')).toBeLessThan(models.indexOf('gemini-2.5-flash-lite'))
+ })
+
it('whitelist 模式会忽略通配符条目', () => {
const mapping = buildModelMappingObject('whitelist', ['claude-*', 'gemini-3.1-flash-image'], [])
expect(mapping).toEqual({
diff --git a/frontend/src/composables/useModelWhitelist.ts b/frontend/src/composables/useModelWhitelist.ts
index df59f8de..09a150cb 100644
--- a/frontend/src/composables/useModelWhitelist.ts
+++ b/frontend/src/composables/useModelWhitelist.ts
@@ -51,6 +51,8 @@ export const claudeModels = [
const geminiModels = [
// Keep in sync with backend curated Gemini lists.
// This list is intentionally conservative (models commonly available across OAuth/API key).
+ 'gemini-3.1-flash-image',
+ 'gemini-2.5-flash-image',
'gemini-2.0-flash',
'gemini-2.5-flash',
'gemini-2.5-pro',
@@ -85,6 +87,8 @@ const antigravityModels = [
'claude-sonnet-4-5',
'claude-sonnet-4-5-thinking',
// Gemini 2.5 系列
+ 'gemini-3.1-flash-image',
+ 'gemini-2.5-flash-image',
'gemini-2.5-flash',
'gemini-2.5-flash-lite',
'gemini-2.5-flash-thinking',
@@ -96,7 +100,6 @@ const antigravityModels = [
// Gemini 3.1 系列
'gemini-3.1-pro-high',
'gemini-3.1-pro-low',
- 'gemini-3.1-flash-image',
'gemini-3-pro-image',
// 其他
'gpt-oss-120b-medium',
@@ -291,7 +294,9 @@ const soraPresetMappings: { label: string; from: string; to: string; color: stri
const geminiPresetMappings = [
{ label: 'Flash 2.0', from: 'gemini-2.0-flash', to: 'gemini-2.0-flash', color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400' },
{ label: '2.5 Flash', from: 'gemini-2.5-flash', to: 'gemini-2.5-flash', color: 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400' },
- { label: '2.5 Pro', from: 'gemini-2.5-pro', to: 'gemini-2.5-pro', color: 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' }
+ { label: '2.5 Image', from: 'gemini-2.5-flash-image', to: 'gemini-2.5-flash-image', color: 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-400' },
+ { label: '2.5 Pro', from: 'gemini-2.5-pro', to: 'gemini-2.5-pro', color: 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' },
+ { label: '3.1 Image', from: 'gemini-3.1-flash-image', to: 'gemini-3.1-flash-image', color: 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-400' }
]
// Antigravity 预设映射(支持通配符)
@@ -314,6 +319,9 @@ const antigravityPresetMappings = [
// Gemini 通配符映射
{ label: 'Gemini 3→Flash', from: 'gemini-3*', to: 'gemini-3-flash', color: 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-400' },
{ label: 'Gemini 2.5→Flash', from: 'gemini-2.5*', to: 'gemini-2.5-flash', color: 'bg-orange-100 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/30 dark:text-orange-400' },
+ { label: '2.5-Flash-Image透传', from: 'gemini-2.5-flash-image', to: 'gemini-2.5-flash-image', color: 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-400' },
+ { label: '3.1-Flash-Image透传', from: 'gemini-3.1-flash-image', to: 'gemini-3.1-flash-image', color: 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-400' },
+ { label: '3-Pro-Image→3.1', from: 'gemini-3-pro-image', to: 'gemini-3.1-flash-image', color: 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-400' },
{ label: '3-Flash透传', from: 'gemini-3-flash', to: 'gemini-3-flash', color: 'bg-lime-100 text-lime-700 hover:bg-lime-200 dark:bg-lime-900/30 dark:text-lime-400' },
{ label: '2.5-Flash-Lite透传', from: 'gemini-2.5-flash-lite', to: 'gemini-2.5-flash-lite', color: 'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400' },
// 精确映射
diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts
index 8809469b..2b546c28 100644
--- a/frontend/src/i18n/locales/en.ts
+++ b/frontend/src/i18n/locales/en.ts
@@ -2416,6 +2416,7 @@ export default {
connectedToApi: 'Connected to API',
usingModel: 'Using model: {model}',
sendingTestMessage: 'Sending test message: "hi"',
+ sendingGeminiImageRequest: 'Sending Gemini image generation test request...',
response: 'Response:',
startTest: 'Start Test',
testing: 'Testing...',
@@ -2427,6 +2428,13 @@ export default {
selectTestModel: 'Select Test Model',
testModel: 'Test model',
testPrompt: 'Prompt: "hi"',
+ geminiImagePromptLabel: 'Image prompt',
+ geminiImagePromptPlaceholder: 'Example: Generate an orange cat astronaut sticker in pixel-art style on a solid background.',
+ geminiImagePromptDefault: 'Generate a cute orange cat astronaut sticker on a clean pastel background.',
+ geminiImageTestHint: 'When a Gemini image model is selected, this test sends a real image-generation request and previews the returned image below.',
+ geminiImageTestMode: 'Mode: Gemini image generation test',
+ geminiImagePreview: 'Generated images:',
+ geminiImageReceived: 'Received test image #{count}',
soraUpstreamBaseUrlHint: 'Upstream Sora service URL (another Sub2API instance or compatible API)',
soraTestHint: 'Sora test runs connectivity and capability checks (/backend/me, subscription, Sora2 invite and remaining quota).',
soraTestTarget: 'Target: Sora account capability',
diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts
index 07a0187b..308affbb 100644
--- a/frontend/src/i18n/locales/zh.ts
+++ b/frontend/src/i18n/locales/zh.ts
@@ -2545,6 +2545,7 @@ export default {
connectedToApi: '已连接到 API',
usingModel: '使用模型:{model}',
sendingTestMessage: '发送测试消息:"hi"',
+ sendingGeminiImageRequest: '发送 Gemini 生图测试请求...',
response: '响应:',
startTest: '开始测试',
retry: '重试',
@@ -2555,6 +2556,13 @@ export default {
selectTestModel: '选择测试模型',
testModel: '测试模型',
testPrompt: '提示词:"hi"',
+ geminiImagePromptLabel: '生图提示词',
+ geminiImagePromptPlaceholder: '例如:生成一只戴宇航员头盔的橘猫,像素插画风格,纯色背景。',
+ geminiImagePromptDefault: 'Generate a cute orange cat astronaut sticker on a clean pastel background.',
+ geminiImageTestHint: '选择 Gemini 图片模型后,这里会直接发起生图测试,并在下方展示返回图片。',
+ geminiImageTestMode: '模式:Gemini 生图测试',
+ geminiImagePreview: '生成结果:',
+ geminiImageReceived: '已收到第 {count} 张测试图片',
soraUpstreamBaseUrlHint: '上游 Sora 服务地址(另一个 Sub2API 实例或兼容 API)',
soraTestHint: 'Sora 测试将执行连通性与能力检测(/backend/me、订阅信息、Sora2 邀请码与剩余额度)。',
soraTestTarget: '检测目标:Sora 账号能力',