Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
22385be5
Commit
22385be5
authored
Apr 22, 2026
by
IanShaw027
Browse files
Merge remote-tracking branch 'upstream/main' into rebuild/auth-identity-foundation
# Conflicts: # backend/internal/service/openai_images.go
parents
6b194903
1e0d4660
Changes
11
Hide whitespace changes
Inline
Side-by-side
backend/internal/pkg/openai/constants.go
View file @
22385be5
...
...
@@ -20,6 +20,8 @@ var DefaultModels = []Model{
{
ID
:
"gpt-5.3-codex"
,
Object
:
"model"
,
Created
:
1735689600
,
OwnedBy
:
"openai"
,
Type
:
"model"
,
DisplayName
:
"GPT-5.3 Codex"
},
{
ID
:
"gpt-5.3-codex-spark"
,
Object
:
"model"
,
Created
:
1735689600
,
OwnedBy
:
"openai"
,
Type
:
"model"
,
DisplayName
:
"GPT-5.3 Codex Spark"
},
{
ID
:
"gpt-5.2"
,
Object
:
"model"
,
Created
:
1733875200
,
OwnedBy
:
"openai"
,
Type
:
"model"
,
DisplayName
:
"GPT-5.2"
},
{
ID
:
"gpt-image-1"
,
Object
:
"model"
,
Created
:
1733875200
,
OwnedBy
:
"openai"
,
Type
:
"model"
,
DisplayName
:
"GPT Image 1"
},
{
ID
:
"gpt-image-1.5"
,
Object
:
"model"
,
Created
:
1735689600
,
OwnedBy
:
"openai"
,
Type
:
"model"
,
DisplayName
:
"GPT Image 1.5"
},
{
ID
:
"gpt-image-2"
,
Object
:
"model"
,
Created
:
1738368000
,
OwnedBy
:
"openai"
,
Type
:
"model"
,
DisplayName
:
"GPT Image 2"
},
}
...
...
backend/internal/service/account_test_service.go
View file @
22385be5
...
...
@@ -5,6 +5,7 @@ import (
"bytes"
"context"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
...
...
@@ -52,8 +53,14 @@ type TestEvent struct {
const
(
defaultGeminiTextTestPrompt
=
"hi"
defaultGeminiImageTestPrompt
=
"Generate a cute orange cat astronaut sticker on a clean pastel background."
defaultOpenAIImageTestPrompt
=
"Generate a cute orange cat astronaut sticker on a clean pastel background."
)
// isOpenAIImageModel checks if the model is an OpenAI image generation model (e.g. gpt-image-2).
func
isOpenAIImageModel
(
model
string
)
bool
{
return
strings
.
HasPrefix
(
strings
.
ToLower
(
model
),
"gpt-image-"
)
}
// AccountTestService handles account testing operations
type
AccountTestService
struct
{
accountRepo
AccountRepository
...
...
@@ -430,6 +437,18 @@ func (s *AccountTestService) testOpenAIAccountConnection(c *gin.Context, account
}
}
// Route to image generation test if an image model is selected
if
isOpenAIImageModel
(
testModelID
)
{
imagePrompt
:=
strings
.
TrimSpace
(
prompt
)
if
imagePrompt
==
""
{
imagePrompt
=
defaultOpenAIImageTestPrompt
}
if
account
.
Type
==
"apikey"
{
return
s
.
testOpenAIImageAPIKey
(
c
,
ctx
,
account
,
testModelID
,
imagePrompt
)
}
return
s
.
testOpenAIImageOAuth
(
c
,
ctx
,
account
,
testModelID
,
imagePrompt
)
}
// Determine authentication method and API URL
var
authToken
string
var
apiURL
string
...
...
@@ -1026,7 +1045,336 @@ func (s *AccountTestService) processOpenAIStream(c *gin.Context, body io.Reader)
}
}
// sendEvent sends a SSE event to the client
// testOpenAIImageAPIKey tests OpenAI image generation using an API Key account.
func
(
s
*
AccountTestService
)
testOpenAIImageAPIKey
(
c
*
gin
.
Context
,
ctx
context
.
Context
,
account
*
Account
,
modelID
,
prompt
string
)
error
{
authToken
:=
account
.
GetOpenAIApiKey
()
if
authToken
==
""
{
return
s
.
sendErrorAndEnd
(
c
,
"No API key available"
)
}
baseURL
:=
account
.
GetOpenAIBaseURL
()
if
baseURL
==
""
{
baseURL
=
"https://api.openai.com"
}
normalizedBaseURL
,
err
:=
s
.
validateUpstreamBaseURL
(
baseURL
)
if
err
!=
nil
{
return
s
.
sendErrorAndEnd
(
c
,
fmt
.
Sprintf
(
"Invalid base URL: %s"
,
err
.
Error
()))
}
apiURL
:=
strings
.
TrimSuffix
(
normalizedBaseURL
,
"/"
)
+
"/v1/images/generations"
// Set SSE headers
c
.
Writer
.
Header
()
.
Set
(
"Content-Type"
,
"text/event-stream"
)
c
.
Writer
.
Header
()
.
Set
(
"Cache-Control"
,
"no-cache"
)
c
.
Writer
.
Header
()
.
Set
(
"Connection"
,
"keep-alive"
)
c
.
Writer
.
Header
()
.
Set
(
"X-Accel-Buffering"
,
"no"
)
c
.
Writer
.
Flush
()
s
.
sendEvent
(
c
,
TestEvent
{
Type
:
"test_start"
,
Model
:
modelID
})
payload
:=
map
[
string
]
any
{
"model"
:
modelID
,
"prompt"
:
prompt
,
"n"
:
1
,
"response_format"
:
"b64_json"
,
}
payloadBytes
,
_
:=
json
.
Marshal
(
payload
)
req
,
err
:=
http
.
NewRequestWithContext
(
ctx
,
"POST"
,
apiURL
,
bytes
.
NewReader
(
payloadBytes
))
if
err
!=
nil
{
return
s
.
sendErrorAndEnd
(
c
,
"Failed to create request"
)
}
req
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
req
.
Header
.
Set
(
"Authorization"
,
"Bearer "
+
authToken
)
proxyURL
:=
""
if
account
.
ProxyID
!=
nil
&&
account
.
Proxy
!=
nil
{
proxyURL
=
account
.
Proxy
.
URL
()
}
resp
,
err
:=
s
.
httpUpstream
.
DoWithTLS
(
req
,
proxyURL
,
account
.
ID
,
account
.
Concurrency
,
s
.
tlsFPProfileService
.
ResolveTLSProfile
(
account
))
if
err
!=
nil
{
return
s
.
sendErrorAndEnd
(
c
,
fmt
.
Sprintf
(
"Request failed: %s"
,
err
.
Error
()))
}
defer
func
()
{
_
=
resp
.
Body
.
Close
()
}()
body
,
err
:=
io
.
ReadAll
(
resp
.
Body
)
if
err
!=
nil
{
return
s
.
sendErrorAndEnd
(
c
,
fmt
.
Sprintf
(
"Failed to read response: %s"
,
err
.
Error
()))
}
if
resp
.
StatusCode
!=
http
.
StatusOK
{
return
s
.
sendErrorAndEnd
(
c
,
fmt
.
Sprintf
(
"API returned %d: %s"
,
resp
.
StatusCode
,
string
(
body
)))
}
// Parse {"data": [{"b64_json": "...", "revised_prompt": "..."}]}
var
result
struct
{
Data
[]
struct
{
B64JSON
string
`json:"b64_json"`
RevisedPrompt
string
`json:"revised_prompt"`
}
`json:"data"`
}
if
err
:=
json
.
Unmarshal
(
body
,
&
result
);
err
!=
nil
{
return
s
.
sendErrorAndEnd
(
c
,
fmt
.
Sprintf
(
"Failed to parse response: %s"
,
err
.
Error
()))
}
if
len
(
result
.
Data
)
==
0
{
return
s
.
sendErrorAndEnd
(
c
,
"No images returned from API"
)
}
for
_
,
item
:=
range
result
.
Data
{
if
item
.
RevisedPrompt
!=
""
{
s
.
sendEvent
(
c
,
TestEvent
{
Type
:
"content"
,
Text
:
item
.
RevisedPrompt
})
}
if
item
.
B64JSON
!=
""
{
s
.
sendEvent
(
c
,
TestEvent
{
Type
:
"image"
,
ImageURL
:
"data:image/png;base64,"
+
item
.
B64JSON
,
MimeType
:
"image/png"
,
})
}
}
s
.
sendEvent
(
c
,
TestEvent
{
Type
:
"test_complete"
,
Success
:
true
})
return
nil
}
// testOpenAIImageOAuth tests OpenAI image generation using an OAuth account via ChatGPT backend API.
func
(
s
*
AccountTestService
)
testOpenAIImageOAuth
(
c
*
gin
.
Context
,
ctx
context
.
Context
,
account
*
Account
,
modelID
,
prompt
string
)
error
{
authToken
:=
account
.
GetOpenAIAccessToken
()
if
authToken
==
""
{
return
s
.
sendErrorAndEnd
(
c
,
"No access token available"
)
}
// Set SSE headers
c
.
Writer
.
Header
()
.
Set
(
"Content-Type"
,
"text/event-stream"
)
c
.
Writer
.
Header
()
.
Set
(
"Cache-Control"
,
"no-cache"
)
c
.
Writer
.
Header
()
.
Set
(
"Connection"
,
"keep-alive"
)
c
.
Writer
.
Header
()
.
Set
(
"X-Accel-Buffering"
,
"no"
)
c
.
Writer
.
Flush
()
s
.
sendEvent
(
c
,
TestEvent
{
Type
:
"test_start"
,
Model
:
modelID
})
s
.
sendEvent
(
c
,
TestEvent
{
Type
:
"content"
,
Text
:
"Initializing ChatGPT backend...
\n
"
})
// Build headers (replicating buildOpenAIBackendAPIHeaders logic)
headers
:=
buildOpenAIBackendAPIHeadersForTest
(
ctx
,
account
,
authToken
,
s
.
accountRepo
)
proxyURL
:=
""
if
account
.
ProxyID
!=
nil
&&
account
.
Proxy
!=
nil
{
proxyURL
=
account
.
Proxy
.
URL
()
}
client
,
err
:=
newOpenAIBackendAPIClient
(
proxyURL
)
if
err
!=
nil
{
return
s
.
sendErrorAndEnd
(
c
,
fmt
.
Sprintf
(
"Failed to create client: %s"
,
err
.
Error
()))
}
// Bootstrap
if
bootstrapErr
:=
bootstrapOpenAIBackendAPI
(
ctx
,
client
,
headers
);
bootstrapErr
!=
nil
{
log
.
Printf
(
"OpenAI image test bootstrap warning: %v"
,
bootstrapErr
)
}
// Fetch chat requirements
s
.
sendEvent
(
c
,
TestEvent
{
Type
:
"content"
,
Text
:
"Fetching chat requirements...
\n
"
})
chatReqs
,
err
:=
fetchOpenAIChatRequirements
(
ctx
,
client
,
headers
)
if
err
!=
nil
{
return
s
.
sendErrorAndEnd
(
c
,
fmt
.
Sprintf
(
"Chat requirements failed: %s"
,
err
.
Error
()))
}
if
chatReqs
.
Arkose
.
Required
{
return
s
.
sendErrorAndEnd
(
c
,
"Unsupported challenge: arkose required"
)
}
// Initialize and prepare conversation
s
.
sendEvent
(
c
,
TestEvent
{
Type
:
"content"
,
Text
:
"Preparing image conversation...
\n
"
})
parentMessageID
:=
uuid
.
NewString
()
proofToken
:=
generateOpenAIProofToken
(
chatReqs
.
ProofOfWork
.
Required
,
chatReqs
.
ProofOfWork
.
Seed
,
chatReqs
.
ProofOfWork
.
Difficulty
,
headers
.
Get
(
"User-Agent"
))
_
=
initializeOpenAIImageConversation
(
ctx
,
client
,
headers
)
conduitToken
,
err
:=
prepareOpenAIImageConversation
(
ctx
,
client
,
headers
,
prompt
,
parentMessageID
,
chatReqs
.
Token
,
proofToken
)
if
err
!=
nil
{
return
s
.
sendErrorAndEnd
(
c
,
fmt
.
Sprintf
(
"Conversation prepare failed: %s"
,
err
.
Error
()))
}
// Build simplified conversation request (no file uploads)
convReq
:=
buildOpenAIImageTestConversationRequest
(
prompt
,
parentMessageID
)
convHeaders
:=
cloneHTTPHeader
(
headers
)
convHeaders
.
Set
(
"Accept"
,
"text/event-stream"
)
convHeaders
.
Set
(
"Content-Type"
,
"application/json"
)
convHeaders
.
Set
(
"openai-sentinel-chat-requirements-token"
,
chatReqs
.
Token
)
if
conduitToken
!=
""
{
convHeaders
.
Set
(
"x-conduit-token"
,
conduitToken
)
}
if
proofToken
!=
""
{
convHeaders
.
Set
(
"openai-sentinel-proof-token"
,
proofToken
)
}
s
.
sendEvent
(
c
,
TestEvent
{
Type
:
"content"
,
Text
:
"Generating image...
\n
"
})
resp
,
err
:=
client
.
R
()
.
SetContext
(
ctx
)
.
DisableAutoReadResponse
()
.
SetHeaders
(
headerToMap
(
convHeaders
))
.
SetBodyJsonMarshal
(
convReq
)
.
Post
(
openAIChatGPTConversationURL
)
if
err
!=
nil
{
return
s
.
sendErrorAndEnd
(
c
,
fmt
.
Sprintf
(
"Conversation request failed: %s"
,
err
.
Error
()))
}
defer
func
()
{
if
resp
!=
nil
&&
resp
.
Body
!=
nil
{
_
=
resp
.
Body
.
Close
()
}
}()
if
resp
.
StatusCode
>=
400
{
return
s
.
sendErrorAndEnd
(
c
,
fmt
.
Sprintf
(
"Conversation API returned %d"
,
resp
.
StatusCode
))
}
startTime
:=
time
.
Now
()
conversationID
,
pointerInfos
,
_
,
_
,
err
:=
readOpenAIImageConversationStream
(
resp
,
startTime
)
if
err
!=
nil
{
return
s
.
sendErrorAndEnd
(
c
,
fmt
.
Sprintf
(
"Stream read failed: %s"
,
err
.
Error
()))
}
pointerInfos
=
mergeOpenAIImagePointerInfos
(
pointerInfos
,
nil
)
if
conversationID
!=
""
&&
!
hasOpenAIFileServicePointerInfos
(
pointerInfos
)
{
s
.
sendEvent
(
c
,
TestEvent
{
Type
:
"content"
,
Text
:
"Waiting for image generation to complete...
\n
"
})
polledPointers
,
pollErr
:=
pollOpenAIImageConversation
(
ctx
,
client
,
headers
,
conversationID
)
if
pollErr
!=
nil
{
return
s
.
sendErrorAndEnd
(
c
,
fmt
.
Sprintf
(
"Poll failed: %s"
,
pollErr
.
Error
()))
}
pointerInfos
=
mergeOpenAIImagePointerInfos
(
pointerInfos
,
polledPointers
)
}
pointerInfos
=
preferOpenAIFileServicePointerInfos
(
pointerInfos
)
if
len
(
pointerInfos
)
==
0
{
return
s
.
sendErrorAndEnd
(
c
,
"No images returned from conversation"
)
}
s
.
sendEvent
(
c
,
TestEvent
{
Type
:
"content"
,
Text
:
"Downloading generated image...
\n
"
})
// Download and encode each image
for
_
,
pointer
:=
range
pointerInfos
{
downloadURL
,
err
:=
fetchOpenAIImageDownloadURL
(
ctx
,
client
,
headers
,
conversationID
,
pointer
.
Pointer
)
if
err
!=
nil
{
return
s
.
sendErrorAndEnd
(
c
,
fmt
.
Sprintf
(
"Download URL fetch failed: %s"
,
err
.
Error
()))
}
data
,
err
:=
downloadOpenAIImageBytes
(
ctx
,
client
,
headers
,
downloadURL
)
if
err
!=
nil
{
return
s
.
sendErrorAndEnd
(
c
,
fmt
.
Sprintf
(
"Image download failed: %s"
,
err
.
Error
()))
}
b64
:=
base64
.
StdEncoding
.
EncodeToString
(
data
)
mimeType
:=
http
.
DetectContentType
(
data
)
if
pointer
.
Prompt
!=
""
{
s
.
sendEvent
(
c
,
TestEvent
{
Type
:
"content"
,
Text
:
pointer
.
Prompt
})
}
s
.
sendEvent
(
c
,
TestEvent
{
Type
:
"image"
,
ImageURL
:
"data:"
+
mimeType
+
";base64,"
+
b64
,
MimeType
:
mimeType
,
})
}
s
.
sendEvent
(
c
,
TestEvent
{
Type
:
"test_complete"
,
Success
:
true
})
return
nil
}
// buildOpenAIBackendAPIHeadersForTest builds ChatGPT backend API headers for test purposes.
// Replicates the logic from OpenAIGatewayService.buildOpenAIBackendAPIHeaders without
// requiring the full gateway service dependency.
func
buildOpenAIBackendAPIHeadersForTest
(
ctx
context
.
Context
,
account
*
Account
,
token
string
,
repo
AccountRepository
)
http
.
Header
{
// Ensure device and session IDs exist
deviceID
:=
account
.
GetOpenAIDeviceID
()
sessionID
:=
account
.
GetOpenAISessionID
()
if
deviceID
==
""
||
sessionID
==
""
{
updates
:=
map
[
string
]
any
{}
if
deviceID
==
""
{
deviceID
=
uuid
.
NewString
()
updates
[
"openai_device_id"
]
=
deviceID
}
if
sessionID
==
""
{
sessionID
=
uuid
.
NewString
()
updates
[
"openai_session_id"
]
=
sessionID
}
if
account
.
Extra
==
nil
{
account
.
Extra
=
map
[
string
]
any
{}
}
for
key
,
value
:=
range
updates
{
account
.
Extra
[
key
]
=
value
}
if
repo
!=
nil
{
updateCtx
,
cancel
:=
context
.
WithTimeout
(
ctx
,
5
*
time
.
Second
)
defer
cancel
()
_
=
repo
.
UpdateExtra
(
updateCtx
,
account
.
ID
,
updates
)
}
}
headers
:=
make
(
http
.
Header
)
headers
.
Set
(
"Authorization"
,
"Bearer "
+
token
)
headers
.
Set
(
"Accept"
,
"application/json"
)
headers
.
Set
(
"Origin"
,
"https://chatgpt.com"
)
headers
.
Set
(
"Referer"
,
"https://chatgpt.com/"
)
headers
.
Set
(
"Sec-Fetch-Dest"
,
"empty"
)
headers
.
Set
(
"Sec-Fetch-Mode"
,
"cors"
)
headers
.
Set
(
"Sec-Fetch-Site"
,
"same-origin"
)
headers
.
Set
(
"User-Agent"
,
openAIImageBackendUserAgent
)
if
customUA
:=
strings
.
TrimSpace
(
account
.
GetOpenAIUserAgent
());
customUA
!=
""
{
headers
.
Set
(
"User-Agent"
,
customUA
)
}
if
chatgptAccountID
:=
strings
.
TrimSpace
(
account
.
GetChatGPTAccountID
());
chatgptAccountID
!=
""
{
headers
.
Set
(
"chatgpt-account-id"
,
chatgptAccountID
)
}
if
deviceID
!=
""
{
headers
.
Set
(
"oai-device-id"
,
deviceID
)
headers
.
Set
(
"Cookie"
,
"oai-did="
+
deviceID
)
}
if
sessionID
!=
""
{
headers
.
Set
(
"oai-session-id"
,
sessionID
)
}
return
headers
}
// buildOpenAIImageTestConversationRequest creates a simplified image generation conversation request.
func
buildOpenAIImageTestConversationRequest
(
prompt
,
parentMessageID
string
)
map
[
string
]
any
{
promptText
:=
strings
.
TrimSpace
(
prompt
)
if
promptText
==
""
{
promptText
=
"Generate an image."
}
metadata
:=
map
[
string
]
any
{
"developer_mode_connector_ids"
:
[]
any
{},
"selected_github_repos"
:
[]
any
{},
"selected_all_github_repos"
:
false
,
"system_hints"
:
[]
string
{
"picture_v2"
},
"serialization_metadata"
:
map
[
string
]
any
{
"custom_symbol_offsets"
:
[]
any
{},
},
}
message
:=
map
[
string
]
any
{
"id"
:
uuid
.
NewString
(),
"author"
:
map
[
string
]
any
{
"role"
:
"user"
},
"content"
:
map
[
string
]
any
{
"content_type"
:
"text"
,
"parts"
:
[]
any
{
promptText
},
},
"metadata"
:
metadata
,
"create_time"
:
float64
(
time
.
Now
()
.
UnixMilli
())
/
1000
,
}
return
map
[
string
]
any
{
"action"
:
"next"
,
"client_prepare_state"
:
"sent"
,
"parent_message_id"
:
parentMessageID
,
"messages"
:
[]
any
{
message
},
"model"
:
"auto"
,
"timezone_offset_min"
:
openAITimezoneOffsetMinutes
(),
"timezone"
:
openAITimezoneName
(),
"conversation_mode"
:
map
[
string
]
any
{
"kind"
:
"primary_assistant"
},
"system_hints"
:
[]
string
{
"picture_v2"
},
"supports_buffering"
:
true
,
"supported_encodings"
:
[]
string
{
"v1"
},
"client_contextual_info"
:
map
[
string
]
any
{
"app_name"
:
"chatgpt.com"
},
"force_nulligen"
:
false
,
"force_paragen"
:
false
,
"force_paragen_model_slug"
:
""
,
"force_rate_limit"
:
false
,
"websocket_request_id"
:
uuid
.
NewString
(),
}
}
func
(
s
*
AccountTestService
)
sendEvent
(
c
*
gin
.
Context
,
event
TestEvent
)
{
eventJSON
,
_
:=
json
.
Marshal
(
event
)
if
_
,
err
:=
fmt
.
Fprintf
(
c
.
Writer
,
"data: %s
\n\n
"
,
eventJSON
);
err
!=
nil
{
...
...
backend/internal/service/openai_account_scheduler.go
View file @
22385be5
...
...
@@ -917,7 +917,15 @@ func (s *OpenAIGatewayService) SelectAccountWithSchedulerForImages(
excludedIDs
map
[
int64
]
struct
{},
requiredCapability
OpenAIImagesCapability
,
)
(
*
AccountSelectionResult
,
OpenAIAccountScheduleDecision
,
error
)
{
return
s
.
selectAccountWithScheduler
(
ctx
,
groupID
,
""
,
sessionHash
,
requestedModel
,
excludedIDs
,
OpenAIUpstreamTransportHTTPSSE
,
requiredCapability
)
selection
,
decision
,
err
:=
s
.
selectAccountWithScheduler
(
ctx
,
groupID
,
""
,
sessionHash
,
requestedModel
,
excludedIDs
,
OpenAIUpstreamTransportHTTPSSE
,
requiredCapability
)
if
err
==
nil
&&
selection
!=
nil
&&
selection
.
Account
!=
nil
{
return
selection
,
decision
,
nil
}
// 如果要求 native 能力(如指定了模型)但没有可用的 APIKey 账号,回退到 basic(OAuth 账号)
if
requiredCapability
==
OpenAIImagesCapabilityNative
{
return
s
.
selectAccountWithScheduler
(
ctx
,
groupID
,
""
,
sessionHash
,
requestedModel
,
excludedIDs
,
OpenAIUpstreamTransportHTTPSSE
,
OpenAIImagesCapabilityBasic
)
}
return
selection
,
decision
,
err
}
func
(
s
*
OpenAIGatewayService
)
selectAccountWithScheduler
(
...
...
backend/internal/service/openai_images.go
View file @
22385be5
...
...
@@ -1294,7 +1294,7 @@ type openAIImageToolMessage struct {
}
func
readOpenAIImageConversationStream
(
resp
*
req
.
Response
,
startTime
time
.
Time
)
(
string
,
[]
openAIImagePointerInfo
,
OpenAIUsage
,
*
int
,
error
)
{
if
resp
==
nil
||
resp
.
Response
==
nil
||
resp
.
Body
==
nil
{
if
resp
==
nil
||
resp
.
Body
==
nil
{
return
""
,
nil
,
OpenAIUsage
{},
nil
,
fmt
.
Errorf
(
"empty conversation response"
)
}
reader
:=
bufio
.
NewReader
(
resp
.
Body
)
...
...
frontend/src/components/account/AccountTestModal.vue
View file @
22385be5
...
...
@@ -55,12 +55,12 @@
/>
</div>
<div
v-if=
"supports
Gemini
ImageTest"
class=
"space-y-1.5"
>
<div
v-if=
"supportsImageTest"
class=
"space-y-1.5"
>
<TextArea
v-model=
"testPrompt"
:label=
"t('admin.accounts.
geminiI
magePromptLabel')"
:placeholder=
"t('admin.accounts.
geminiI
magePromptPlaceholder')"
:hint=
"t('admin.accounts.
geminiI
mageTestHint')"
:label=
"t('admin.accounts.
i
magePromptLabel')"
:placeholder=
"t('admin.accounts.
i
magePromptPlaceholder')"
:hint=
"t('admin.accounts.
i
mageTestHint')"
:disabled=
"status === 'connecting'"
rows=
"3"
/>
...
...
@@ -122,25 +122,49 @@
<div
v-if=
"generatedImages.length > 0"
class=
"space-y-2"
>
<div
class=
"text-xs font-medium text-gray-600 dark:text-gray-300"
>
{{
t
(
'
admin.accounts.
geminiI
magePreview
'
)
}}
{{
t
(
'
admin.accounts.
i
magePreview
'
)
}}
</div>
<div
class=
"
grid gap-3 sm:grid-cols-2
"
>
<
a
<div
class=
"
flex flex-wrap justify-center gap-3
"
>
<
div
v-for=
"(image, index) in generatedImages"
:key=
"`$
{image.url}-${index}`"
:href="image.url"
target="_blank"
rel="noopener noreferrer"
class="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm transition hover:border-primary-300 hover:shadow-md dark:border-dark-500 dark:bg-dark-700"
class="group/img relative cursor-pointer overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm transition hover:border-primary-300 hover:shadow-md dark:border-dark-500 dark:bg-dark-700"
@click="previewImageUrl = image.url"
>
<img
:src=
"image.url"
:alt=
"`gemini-test-image-$
{index + 1}`" class="h-48 w-full object-cover" />
<div
class=
"border-t border-gray-100 px-3 py-2 text-xs text-gray-500 dark:border-dark-500 dark:text-gray-300"
>
<img
:src=
"image.url"
:alt=
"`test-image-$
{index + 1}`" class="max-h-[360px] w-full object-contain" />
<div
class=
"absolute inset-0 flex items-center justify-center bg-black/0 transition-colors group-hover/img:bg-black/20"
>
<Icon
name=
"eye"
size=
"lg"
class=
"text-white opacity-0 drop-shadow-lg transition-opacity group-hover/img:opacity-100"
:stroke-width=
"2"
/>
</div>
<div
class=
"border-t border-gray-100 px-3 py-1.5 text-xs text-gray-500 dark:border-dark-500 dark:text-gray-300"
>
{{
image
.
mimeType
||
'
image/*
'
}}
</div>
</
a
>
</
div
>
</div>
</div>
<!-- Image Lightbox -->
<Teleport
to=
"body"
>
<Transition
name=
"fade"
>
<div
v-if=
"previewImageUrl"
class=
"fixed inset-0 z-[100] flex items-center justify-center bg-black/80 p-4"
@
click.self=
"previewImageUrl = ''"
>
<button
class=
"absolute right-4 top-4 rounded-full bg-black/50 p-2 text-white transition-colors hover:bg-black/70"
@
click=
"previewImageUrl = ''"
>
<Icon
name=
"x"
size=
"lg"
:stroke-width=
"2"
/>
</button>
<img
:src=
"previewImageUrl"
alt=
"preview"
class=
"max-h-[90vh] max-w-[90vw] rounded-lg object-contain shadow-2xl"
/>
</div>
</Transition>
</Teleport>
<!-- Test Info -->
<div
class=
"flex items-center justify-between px-1 text-xs text-gray-500 dark:text-gray-400"
>
<div
class=
"flex items-center gap-3"
>
...
...
@@ -152,8 +176,8 @@
<span
class=
"flex items-center gap-1"
>
<Icon
name=
"chat"
size=
"sm"
:stroke-width=
"2"
/>
{{
supports
Gemini
ImageTest
?
t
(
'
admin.accounts.
geminiI
mageTestMode
'
)
supportsImageTest
?
t
(
'
admin.accounts.
i
mageTestMode
'
)
:
t
(
'
admin.accounts.testPrompt
'
)
}}
</span>
...
...
@@ -250,6 +274,7 @@ const testPrompt = ref('')
const
loadingModels
=
ref
(
false
)
let
abortController
:
AbortController
|
null
=
null
const
generatedImages
=
ref
<
PreviewImage
[]
>
([])
const
previewImageUrl
=
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
(()
=>
{
const
modelID
=
selectedModelId
.
value
.
toLowerCase
()
...
...
@@ -258,6 +283,14 @@ const supportsGeminiImageTest = computed(() => {
return
props
.
account
?.
platform
===
'
gemini
'
||
(
props
.
account
?.
platform
===
'
antigravity
'
&&
props
.
account
?.
type
===
'
apikey
'
)
})
const
supportsOpenAIImageTest
=
computed
(()
=>
{
const
modelID
=
selectedModelId
.
value
.
toLowerCase
()
if
(
!
modelID
.
startsWith
(
'
gpt-image-
'
))
return
false
return
props
.
account
?.
platform
===
'
openai
'
})
const
supportsImageTest
=
computed
(()
=>
supportsGeminiImageTest
.
value
||
supportsOpenAIImageTest
.
value
)
const
sortTestModels
=
(
models
:
ClaudeModel
[])
=>
{
const
priorityMap
=
new
Map
(
prioritizedGeminiModels
.
map
((
id
,
index
)
=>
[
id
,
index
]))
...
...
@@ -284,8 +317,8 @@ watch(
)
watch
(
selectedModelId
,
()
=>
{
if
(
supports
Gemini
ImageTest
.
value
&&
!
testPrompt
.
value
.
trim
())
{
testPrompt
.
value
=
t
(
'
admin.accounts.
geminiI
magePromptDefault
'
)
if
(
supportsImageTest
.
value
&&
!
testPrompt
.
value
.
trim
())
{
testPrompt
.
value
=
t
(
'
admin.accounts.
i
magePromptDefault
'
)
}
})
...
...
@@ -325,6 +358,7 @@ const resetState = () => {
streamingContent
.
value
=
''
errorMessage
.
value
=
''
generatedImages
.
value
=
[]
previewImageUrl
.
value
=
''
}
const
handleClose
=
()
=>
{
...
...
@@ -377,7 +411,7 @@ const startTest = async () => {
},
body
:
JSON
.
stringify
({
model_id
:
selectedModelId
.
value
,
prompt
:
supports
Gemini
ImageTest
.
value
?
testPrompt
.
value
.
trim
()
:
''
prompt
:
supportsImageTest
.
value
?
testPrompt
.
value
.
trim
()
:
''
}),
signal
:
abortController
.
signal
})
...
...
@@ -444,8 +478,8 @@ const handleEvent = (event: {
addLine
(
t
(
'
admin.accounts.usingModel
'
,
{
model
:
event
.
model
}),
'
text-cyan-400
'
)
}
addLine
(
supports
Gemini
ImageTest
.
value
?
t
(
'
admin.accounts.sending
Gemini
ImageRequest
'
)
supportsImageTest
.
value
?
t
(
'
admin.accounts.sendingImageRequest
'
)
:
t
(
'
admin.accounts.sendingTestMessage
'
),
'
text-gray-400
'
)
...
...
@@ -466,7 +500,7 @@ const handleEvent = (event: {
url
:
event
.
image_url
,
mimeType
:
event
.
mime_type
})
addLine
(
t
(
'
admin.accounts.
geminiI
mageReceived
'
,
{
count
:
generatedImages
.
value
.
length
}),
'
text-purple-300
'
)
addLine
(
t
(
'
admin.accounts.
i
mageReceived
'
,
{
count
:
generatedImages
.
value
.
length
}),
'
text-purple-300
'
)
}
break
...
...
@@ -500,3 +534,14 @@ const copyOutput = () => {
copyToClipboard
(
text
,
t
(
'
admin.accounts.outputCopied
'
))
}
</
script
>
<
style
>
.fade-enter-active
,
.fade-leave-active
{
transition
:
opacity
0.2s
ease
;
}
.fade-enter-from
,
.fade-leave-to
{
opacity
:
0
;
}
</
style
>
frontend/src/components/admin/account/AccountTestModal.vue
View file @
22385be5
...
...
@@ -55,12 +55,12 @@
/>
</div>
<div
v-if=
"supports
Gemini
ImageTest"
class=
"space-y-1.5"
>
<div
v-if=
"supportsImageTest"
class=
"space-y-1.5"
>
<TextArea
v-model=
"testPrompt"
:label=
"t('admin.accounts.
geminiI
magePromptLabel')"
:placeholder=
"t('admin.accounts.
geminiI
magePromptPlaceholder')"
:hint=
"t('admin.accounts.
geminiI
mageTestHint')"
:label=
"t('admin.accounts.
i
magePromptLabel')"
:placeholder=
"t('admin.accounts.
i
magePromptPlaceholder')"
:hint=
"t('admin.accounts.
i
mageTestHint')"
:disabled=
"status === 'connecting'"
rows=
"3"
/>
...
...
@@ -122,25 +122,49 @@
<div
v-if=
"generatedImages.length > 0"
class=
"space-y-2"
>
<div
class=
"text-xs font-medium text-gray-600 dark:text-gray-300"
>
{{
t
(
'
admin.accounts.
geminiI
magePreview
'
)
}}
{{
t
(
'
admin.accounts.
i
magePreview
'
)
}}
</div>
<div
class=
"
grid gap-3 sm:grid-cols-2
"
>
<
a
<div
class=
"
flex flex-wrap justify-center gap-3
"
>
<
div
v-for=
"(image, index) in generatedImages"
:key=
"`$
{image.url}-${index}`"
:href="image.url"
target="_blank"
rel="noopener noreferrer"
class="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm transition hover:border-primary-300 hover:shadow-md dark:border-dark-500 dark:bg-dark-700"
class="group/img relative cursor-pointer overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm transition hover:border-primary-300 hover:shadow-md dark:border-dark-500 dark:bg-dark-700"
@click="previewImageUrl = image.url"
>
<img
:src=
"image.url"
:alt=
"`gemini-test-image-$
{index + 1}`" class="h-48 w-full object-cover" />
<div
class=
"border-t border-gray-100 px-3 py-2 text-xs text-gray-500 dark:border-dark-500 dark:text-gray-300"
>
<img
:src=
"image.url"
:alt=
"`test-image-$
{index + 1}`" class="max-h-[360px] w-full object-contain" />
<div
class=
"absolute inset-0 flex items-center justify-center bg-black/0 transition-colors group-hover/img:bg-black/20"
>
<Icon
name=
"eye"
size=
"lg"
class=
"text-white opacity-0 drop-shadow-lg transition-opacity group-hover/img:opacity-100"
:stroke-width=
"2"
/>
</div>
<div
class=
"border-t border-gray-100 px-3 py-1.5 text-xs text-gray-500 dark:border-dark-500 dark:text-gray-300"
>
{{
image
.
mimeType
||
'
image/*
'
}}
</div>
</
a
>
</
div
>
</div>
</div>
<!-- Image Lightbox -->
<Teleport
to=
"body"
>
<Transition
name=
"fade"
>
<div
v-if=
"previewImageUrl"
class=
"fixed inset-0 z-[100] flex items-center justify-center bg-black/80 p-4"
@
click.self=
"previewImageUrl = ''"
>
<button
class=
"absolute right-4 top-4 rounded-full bg-black/50 p-2 text-white transition-colors hover:bg-black/70"
@
click=
"previewImageUrl = ''"
>
<Icon
name=
"x"
size=
"lg"
:stroke-width=
"2"
/>
</button>
<img
:src=
"previewImageUrl"
alt=
"preview"
class=
"max-h-[90vh] max-w-[90vw] rounded-lg object-contain shadow-2xl"
/>
</div>
</Transition>
</Teleport>
<!-- Test Info -->
<div
class=
"flex items-center justify-between px-1 text-xs text-gray-500 dark:text-gray-400"
>
<div
class=
"flex items-center gap-3"
>
...
...
@@ -152,8 +176,8 @@
<span
class=
"flex items-center gap-1"
>
<Icon
name=
"chat"
size=
"sm"
:stroke-width=
"2"
/>
{{
supports
Gemini
ImageTest
?
t
(
'
admin.accounts.
geminiI
mageTestMode
'
)
supportsImageTest
?
t
(
'
admin.accounts.
i
mageTestMode
'
)
:
t
(
'
admin.accounts.testPrompt
'
)
}}
</span>
...
...
@@ -250,6 +274,7 @@ const testPrompt = ref('')
const
loadingModels
=
ref
(
false
)
let
abortController
:
AbortController
|
null
=
null
const
generatedImages
=
ref
<
PreviewImage
[]
>
([])
const
previewImageUrl
=
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
(()
=>
{
const
modelID
=
selectedModelId
.
value
.
toLowerCase
()
...
...
@@ -258,6 +283,14 @@ const supportsGeminiImageTest = computed(() => {
return
props
.
account
?.
platform
===
'
gemini
'
||
(
props
.
account
?.
platform
===
'
antigravity
'
&&
props
.
account
?.
type
===
'
apikey
'
)
})
const
supportsOpenAIImageTest
=
computed
(()
=>
{
const
modelID
=
selectedModelId
.
value
.
toLowerCase
()
if
(
!
modelID
.
startsWith
(
'
gpt-image-
'
))
return
false
return
props
.
account
?.
platform
===
'
openai
'
})
const
supportsImageTest
=
computed
(()
=>
supportsGeminiImageTest
.
value
||
supportsOpenAIImageTest
.
value
)
const
sortTestModels
=
(
models
:
ClaudeModel
[])
=>
{
const
priorityMap
=
new
Map
(
prioritizedGeminiModels
.
map
((
id
,
index
)
=>
[
id
,
index
]))
...
...
@@ -284,8 +317,8 @@ watch(
)
watch
(
selectedModelId
,
()
=>
{
if
(
supports
Gemini
ImageTest
.
value
&&
!
testPrompt
.
value
.
trim
())
{
testPrompt
.
value
=
t
(
'
admin.accounts.
geminiI
magePromptDefault
'
)
if
(
supportsImageTest
.
value
&&
!
testPrompt
.
value
.
trim
())
{
testPrompt
.
value
=
t
(
'
admin.accounts.
i
magePromptDefault
'
)
}
})
...
...
@@ -325,6 +358,7 @@ const resetState = () => {
streamingContent
.
value
=
''
errorMessage
.
value
=
''
generatedImages
.
value
=
[]
previewImageUrl
.
value
=
''
}
const
handleClose
=
()
=>
{
...
...
@@ -377,7 +411,7 @@ const startTest = async () => {
},
body
:
JSON
.
stringify
({
model_id
:
selectedModelId
.
value
,
prompt
:
supports
Gemini
ImageTest
.
value
?
testPrompt
.
value
.
trim
()
:
''
prompt
:
supportsImageTest
.
value
?
testPrompt
.
value
.
trim
()
:
''
}),
signal
:
abortController
.
signal
})
...
...
@@ -444,8 +478,8 @@ const handleEvent = (event: {
addLine
(
t
(
'
admin.accounts.usingModel
'
,
{
model
:
event
.
model
}),
'
text-cyan-400
'
)
}
addLine
(
supports
Gemini
ImageTest
.
value
?
t
(
'
admin.accounts.sending
Gemini
ImageRequest
'
)
supportsImageTest
.
value
?
t
(
'
admin.accounts.sendingImageRequest
'
)
:
t
(
'
admin.accounts.sendingTestMessage
'
),
'
text-gray-400
'
)
...
...
@@ -466,7 +500,7 @@ const handleEvent = (event: {
url
:
event
.
image_url
,
mimeType
:
event
.
mime_type
})
addLine
(
t
(
'
admin.accounts.
geminiI
mageReceived
'
,
{
count
:
generatedImages
.
value
.
length
}),
'
text-purple-300
'
)
addLine
(
t
(
'
admin.accounts.
i
mageReceived
'
,
{
count
:
generatedImages
.
value
.
length
}),
'
text-purple-300
'
)
}
break
...
...
@@ -500,3 +534,14 @@ const copyOutput = () => {
copyToClipboard
(
text
,
t
(
'
admin.accounts.outputCopied
'
))
}
</
script
>
<
style
>
.fade-enter-active
,
.fade-leave-active
{
transition
:
opacity
0.2s
ease
;
}
.fade-enter-from
,
.fade-leave-to
{
opacity
:
0
;
}
</
style
>
frontend/src/components/admin/account/__tests__/AccountTestModal.spec.ts
View file @
22385be5
...
...
@@ -24,13 +24,13 @@ vi.mock('@/composables/useClipboard', () => ({
vi
.
mock
(
'
vue-i18n
'
,
async
()
=>
{
const
actual
=
await
vi
.
importActual
<
typeof
import
(
'
vue-i18n
'
)
>
(
'
vue-i18n
'
)
const
messages
:
Record
<
string
,
string
>
=
{
'
admin.accounts.
geminiI
magePromptDefault
'
:
'
Generate a cute orange cat astronaut sticker on a clean pastel background.
'
'
admin.accounts.
i
magePromptDefault
'
:
'
Generate a cute orange cat astronaut sticker on a clean pastel background.
'
}
return
{
...
actual
,
useI18n
:
()
=>
({
t
:
(
key
:
string
,
params
?:
Record
<
string
,
string
|
number
>
)
=>
{
if
(
key
===
'
admin.accounts.
geminiI
mageReceived
'
&&
params
?.
count
)
{
if
(
key
===
'
admin.accounts.
i
mageReceived
'
&&
params
?.
count
)
{
return
`received-
${
params
.
count
}
`
}
return
messages
[
key
]
||
key
...
...
@@ -140,7 +140,7 @@ describe('AccountTestModal', () => {
prompt
:
'
draw a tiny orange cat astronaut
'
})
const
preview
=
wrapper
.
find
(
'
img[alt="
gemini-
test-image-1"]
'
)
const
preview
=
wrapper
.
find
(
'
img[alt="test-image-1"]
'
)
expect
(
preview
.
exists
()).
toBe
(
true
)
expect
(
preview
.
attributes
(
'
src
'
)).
toBe
(
'
data:image/png;base64,QUJD
'
)
})
...
...
frontend/src/composables/useModelWhitelist.ts
View file @
22385be5
...
...
@@ -21,7 +21,9 @@ const openaiModels = [
// GPT-5.3 系列
'
gpt-5.3-codex
'
,
'
gpt-5.3-codex-spark
'
,
'
chatgpt-4o-latest
'
,
'
gpt-4o-audio-preview
'
,
'
gpt-4o-realtime-preview
'
'
gpt-4o-audio-preview
'
,
'
gpt-4o-realtime-preview
'
,
// GPT Image 系列
'
gpt-image-1
'
,
'
gpt-image-1.5
'
,
'
gpt-image-2
'
]
// Anthropic Claude
...
...
frontend/src/i18n/locales/en.ts
View file @
22385be5
...
...
@@ -3023,7 +3023,7 @@ export default {
connectedToApi
:
'
Connected to API
'
,
usingModel
:
'
Using model: {model}
'
,
sendingTestMessage
:
'
Sending test message: "hi"
'
,
sending
Gemini
ImageRequest
:
'
Sending
Gemini
image generation test request...
'
,
sendingImageRequest
:
'
Sending image generation test request...
'
,
response
:
'
Response:
'
,
startTest
:
'
Start Test
'
,
testing
:
'
Testing...
'
,
...
...
@@ -3035,13 +3035,13 @@ export default {
selectTestModel
:
'
Select Test Model
'
,
testModel
:
'
Test model
'
,
testPrompt
:
'
Prompt: "hi"
'
,
geminiI
magePromptLabel
:
'
Image prompt
'
,
geminiI
magePromptPlaceholder
:
'
Example: Generate an orange cat astronaut sticker in pixel-art style on a solid background.
'
,
geminiI
magePromptDefault
:
'
Generate a cute orange cat astronaut sticker on a clean pastel background.
'
,
geminiI
mageTestHint
:
'
When a
Gemini
image model is selected, this test sends a real image-generation request and previews the returned image below.
'
,
geminiI
mageTestMode
:
'
Mode:
Gemini i
mage generation test
'
,
geminiI
magePreview
:
'
Generated images:
'
,
geminiI
mageReceived
:
'
Received test image #{count}
'
,
i
magePromptLabel
:
'
Image prompt
'
,
i
magePromptPlaceholder
:
'
Example: Generate an orange cat astronaut sticker in pixel-art style on a solid background.
'
,
i
magePromptDefault
:
'
Generate a cute orange cat astronaut sticker on a clean pastel background.
'
,
i
mageTestHint
:
'
When a
n
image model is selected, this test sends a real image-generation request and previews the returned image below.
'
,
i
mageTestMode
:
'
Mode:
I
mage generation test
'
,
i
magePreview
:
'
Generated images:
'
,
i
mageReceived
:
'
Received test image #{count}
'
,
// Stats Modal
viewStats
:
'
View Stats
'
,
usageStatistics
:
'
Usage Statistics
'
,
...
...
frontend/src/i18n/locales/zh.ts
View file @
22385be5
...
...
@@ -3154,7 +3154,7 @@ export default {
connectedToApi
:
'
已连接到 API
'
,
usingModel
:
'
使用模型:{model}
'
,
sendingTestMessage
:
'
发送测试消息:"hi"
'
,
sending
Gemini
ImageRequest
:
'
发送
Gemini
生图测试请求...
'
,
sendingImageRequest
:
'
发送生图测试请求...
'
,
response
:
'
响应:
'
,
startTest
:
'
开始测试
'
,
retry
:
'
重试
'
,
...
...
@@ -3165,13 +3165,13 @@ export default {
selectTestModel
:
'
选择测试模型
'
,
testModel
:
'
测试模型
'
,
testPrompt
:
'
提示词:"hi"
'
,
geminiI
magePromptLabel
:
'
生图提示词
'
,
geminiI
magePromptPlaceholder
:
'
例如:生成一只戴宇航员头盔的橘猫,像素插画风格,纯色背景。
'
,
geminiI
magePromptDefault
:
'
Generate a cute orange cat astronaut sticker on a clean pastel background.
'
,
geminiI
mageTestHint
:
'
选择
Gemini
图片模型后,这里会直接发起生图测试,并在下方展示返回图片。
'
,
geminiI
mageTestMode
:
'
模式:
Gemini
生图测试
'
,
geminiI
magePreview
:
'
生成结果:
'
,
geminiI
mageReceived
:
'
已收到第 {count} 张测试图片
'
,
i
magePromptLabel
:
'
生图提示词
'
,
i
magePromptPlaceholder
:
'
例如:生成一只戴宇航员头盔的橘猫,像素插画风格,纯色背景。
'
,
i
magePromptDefault
:
'
Generate a cute orange cat astronaut sticker on a clean pastel background.
'
,
i
mageTestHint
:
'
选择图片模型后,这里会直接发起生图测试,并在下方展示返回图片。
'
,
i
mageTestMode
:
'
模式:生图测试
'
,
i
magePreview
:
'
生成结果:
'
,
i
mageReceived
:
'
已收到第 {count} 张测试图片
'
,
// Stats Modal
viewStats
:
'
查看统计
'
,
usageStatistics
:
'
使用统计
'
,
...
...
frontend/src/views/admin/GroupsView.vue
View file @
22385be5
...
...
@@ -628,11 +628,12 @@
</div>
</div>
<!-- 图片生成计费配置
(antigravity 和 gemini 平台)
-->
<!-- 图片生成计费配置 -->
<div
v-if=
"
createForm.platform === 'antigravity' ||
createForm.platform === 'gemini'
createForm.platform === 'gemini' ||
createForm.platform === 'openai'
"
class=
"border-t pt-4"
>
...
...
@@ -1750,11 +1751,12 @@
</div>
</div>
<!-- 图片生成计费配置
(antigravity 和 gemini 平台)
-->
<!-- 图片生成计费配置 -->
<div
v-if=
"
editForm.platform === 'antigravity' ||
editForm.platform === 'gemini'
editForm.platform === 'gemini' ||
editForm.platform === 'openai'
"
class=
"border-t pt-4"
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment