Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
36b71243
authored
Oct 31, 2025
by
Seefs
Committed by
GitHub
Oct 31, 2025
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix veo3 (#2140)
parent
26a21bd2
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
270 additions
and
6 deletions
+270
-6
controller/video_proxy.go
+24
-2
controller/video_proxy_gemini.go
+158
-0
model/task.go
+36
-1
relay/channel/task/gemini/adaptor.go
+50
-2
relay/common/relay_info.go
+1
-0
relay/relay_task.go
+1
-1
No files found.
controller/video_proxy.go
View file @
36b71243
...
@@ -92,8 +92,30 @@ func VideoProxy(c *gin.Context) {
...
@@ -92,8 +92,30 @@ func VideoProxy(c *gin.Context) {
}
}
if
channel
.
Type
==
constant
.
ChannelTypeGemini
{
if
channel
.
Type
==
constant
.
ChannelTypeGemini
{
videoURL
=
fmt
.
Sprintf
(
"%s&key=%s"
,
c
.
Query
(
"url"
),
channel
.
Key
)
apiKey
:=
task
.
PrivateData
.
Key
req
.
Header
.
Set
(
"x-goog-api-key"
,
channel
.
Key
)
if
apiKey
==
""
{
logger
.
LogError
(
c
.
Request
.
Context
(),
fmt
.
Sprintf
(
"Missing stored API key for Gemini task %s"
,
taskID
))
c
.
JSON
(
http
.
StatusInternalServerError
,
gin
.
H
{
"error"
:
gin
.
H
{
"message"
:
"API key not stored for task"
,
"type"
:
"server_error"
,
},
})
return
}
videoURL
,
err
=
getGeminiVideoURL
(
channel
,
task
,
apiKey
)
if
err
!=
nil
{
logger
.
LogError
(
c
.
Request
.
Context
(),
fmt
.
Sprintf
(
"Failed to resolve Gemini video URL for task %s: %s"
,
taskID
,
err
.
Error
()))
c
.
JSON
(
http
.
StatusBadGateway
,
gin
.
H
{
"error"
:
gin
.
H
{
"message"
:
"Failed to resolve Gemini video URL"
,
"type"
:
"server_error"
,
},
})
return
}
req
.
Header
.
Set
(
"x-goog-api-key"
,
apiKey
)
}
else
{
}
else
{
// Default (Sora, etc.): Use original logic
// Default (Sora, etc.): Use original logic
videoURL
=
fmt
.
Sprintf
(
"%s/v1/videos/%s/content"
,
baseURL
,
task
.
TaskID
)
videoURL
=
fmt
.
Sprintf
(
"%s/v1/videos/%s/content"
,
baseURL
,
task
.
TaskID
)
...
...
controller/video_proxy_gemini.go
0 → 100644
View file @
36b71243
package
controller
import
(
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/relay"
)
func
getGeminiVideoURL
(
channel
*
model
.
Channel
,
task
*
model
.
Task
,
apiKey
string
)
(
string
,
error
)
{
if
channel
==
nil
||
task
==
nil
{
return
""
,
fmt
.
Errorf
(
"invalid channel or task"
)
}
if
url
:=
extractGeminiVideoURLFromTaskData
(
task
);
url
!=
""
{
return
ensureAPIKey
(
url
,
apiKey
),
nil
}
baseURL
:=
constant
.
ChannelBaseURLs
[
channel
.
Type
]
if
channel
.
GetBaseURL
()
!=
""
{
baseURL
=
channel
.
GetBaseURL
()
}
adaptor
:=
relay
.
GetTaskAdaptor
(
constant
.
TaskPlatform
(
strconv
.
Itoa
(
channel
.
Type
)))
if
adaptor
==
nil
{
return
""
,
fmt
.
Errorf
(
"gemini task adaptor not found"
)
}
if
apiKey
==
""
{
return
""
,
fmt
.
Errorf
(
"api key not available for task"
)
}
resp
,
err
:=
adaptor
.
FetchTask
(
baseURL
,
apiKey
,
map
[
string
]
any
{
"task_id"
:
task
.
TaskID
,
"action"
:
task
.
Action
,
})
if
err
!=
nil
{
return
""
,
fmt
.
Errorf
(
"fetch task failed: %w"
,
err
)
}
defer
resp
.
Body
.
Close
()
body
,
err
:=
io
.
ReadAll
(
resp
.
Body
)
if
err
!=
nil
{
return
""
,
fmt
.
Errorf
(
"read task response failed: %w"
,
err
)
}
taskInfo
,
parseErr
:=
adaptor
.
ParseTaskResult
(
body
)
if
parseErr
==
nil
&&
taskInfo
!=
nil
&&
taskInfo
.
RemoteUrl
!=
""
{
return
ensureAPIKey
(
taskInfo
.
RemoteUrl
,
apiKey
),
nil
}
if
url
:=
extractGeminiVideoURLFromPayload
(
body
);
url
!=
""
{
return
ensureAPIKey
(
url
,
apiKey
),
nil
}
if
parseErr
!=
nil
{
return
""
,
fmt
.
Errorf
(
"parse task result failed: %w"
,
parseErr
)
}
return
""
,
fmt
.
Errorf
(
"gemini video url not found"
)
}
func
extractGeminiVideoURLFromTaskData
(
task
*
model
.
Task
)
string
{
if
task
==
nil
||
len
(
task
.
Data
)
==
0
{
return
""
}
var
payload
map
[
string
]
any
if
err
:=
json
.
Unmarshal
(
task
.
Data
,
&
payload
);
err
!=
nil
{
return
""
}
return
extractGeminiVideoURLFromMap
(
payload
)
}
func
extractGeminiVideoURLFromPayload
(
body
[]
byte
)
string
{
var
payload
map
[
string
]
any
if
err
:=
json
.
Unmarshal
(
body
,
&
payload
);
err
!=
nil
{
return
""
}
return
extractGeminiVideoURLFromMap
(
payload
)
}
func
extractGeminiVideoURLFromMap
(
payload
map
[
string
]
any
)
string
{
if
payload
==
nil
{
return
""
}
if
uri
,
ok
:=
payload
[
"uri"
]
.
(
string
);
ok
&&
uri
!=
""
{
return
uri
}
if
resp
,
ok
:=
payload
[
"response"
]
.
(
map
[
string
]
any
);
ok
{
if
uri
:=
extractGeminiVideoURLFromResponse
(
resp
);
uri
!=
""
{
return
uri
}
}
return
""
}
func
extractGeminiVideoURLFromResponse
(
resp
map
[
string
]
any
)
string
{
if
resp
==
nil
{
return
""
}
if
gvr
,
ok
:=
resp
[
"generateVideoResponse"
]
.
(
map
[
string
]
any
);
ok
{
if
uri
:=
extractGeminiVideoURLFromGeneratedSamples
(
gvr
);
uri
!=
""
{
return
uri
}
}
if
videos
,
ok
:=
resp
[
"videos"
]
.
([]
any
);
ok
{
for
_
,
video
:=
range
videos
{
if
vm
,
ok
:=
video
.
(
map
[
string
]
any
);
ok
{
if
uri
,
ok
:=
vm
[
"uri"
]
.
(
string
);
ok
&&
uri
!=
""
{
return
uri
}
}
}
}
if
uri
,
ok
:=
resp
[
"video"
]
.
(
string
);
ok
&&
uri
!=
""
{
return
uri
}
if
uri
,
ok
:=
resp
[
"uri"
]
.
(
string
);
ok
&&
uri
!=
""
{
return
uri
}
return
""
}
func
extractGeminiVideoURLFromGeneratedSamples
(
gvr
map
[
string
]
any
)
string
{
if
gvr
==
nil
{
return
""
}
if
samples
,
ok
:=
gvr
[
"generatedSamples"
]
.
([]
any
);
ok
{
for
_
,
sample
:=
range
samples
{
if
sm
,
ok
:=
sample
.
(
map
[
string
]
any
);
ok
{
if
video
,
ok
:=
sm
[
"video"
]
.
(
map
[
string
]
any
);
ok
{
if
uri
,
ok
:=
video
[
"uri"
]
.
(
string
);
ok
&&
uri
!=
""
{
return
uri
}
}
}
}
}
return
""
}
func
ensureAPIKey
(
uri
,
key
string
)
string
{
if
key
==
""
||
uri
==
""
{
return
uri
}
if
strings
.
Contains
(
uri
,
"key="
)
{
return
uri
}
if
strings
.
Contains
(
uri
,
"?"
)
{
return
fmt
.
Sprintf
(
"%s&key=%s"
,
uri
,
key
)
}
return
fmt
.
Sprintf
(
"%s?key=%s"
,
uri
,
key
)
}
model/task.go
View file @
36b71243
...
@@ -57,7 +57,8 @@ type Task struct {
...
@@ -57,7 +57,8 @@ type Task struct {
FinishTime
int64
`json:"finish_time" gorm:"index"`
FinishTime
int64
`json:"finish_time" gorm:"index"`
Progress
string
`json:"progress" gorm:"type:varchar(20);index"`
Progress
string
`json:"progress" gorm:"type:varchar(20);index"`
Properties
Properties
`json:"properties" gorm:"type:json"`
Properties
Properties
`json:"properties" gorm:"type:json"`
// 禁止返回给用户,内部可能包含key等隐私信息
PrivateData
TaskPrivateData
`json:"-" gorm:"column:private_data;type:json"`
Data
json
.
RawMessage
`json:"data" gorm:"type:json"`
Data
json
.
RawMessage
`json:"data" gorm:"type:json"`
}
}
...
@@ -77,13 +78,39 @@ type Properties struct {
...
@@ -77,13 +78,39 @@ type Properties struct {
func
(
m
*
Properties
)
Scan
(
val
interface
{})
error
{
func
(
m
*
Properties
)
Scan
(
val
interface
{})
error
{
bytesValue
,
_
:=
val
.
([]
byte
)
bytesValue
,
_
:=
val
.
([]
byte
)
if
len
(
bytesValue
)
==
0
{
m
.
Input
=
""
return
nil
}
return
json
.
Unmarshal
(
bytesValue
,
m
)
return
json
.
Unmarshal
(
bytesValue
,
m
)
}
}
func
(
m
Properties
)
Value
()
(
driver
.
Value
,
error
)
{
func
(
m
Properties
)
Value
()
(
driver
.
Value
,
error
)
{
if
m
.
Input
==
""
{
return
nil
,
nil
}
return
json
.
Marshal
(
m
)
return
json
.
Marshal
(
m
)
}
}
type
TaskPrivateData
struct
{
Key
string
`json:"key,omitempty"`
}
func
(
p
*
TaskPrivateData
)
Scan
(
val
interface
{})
error
{
bytesValue
,
_
:=
val
.
([]
byte
)
if
len
(
bytesValue
)
==
0
{
return
nil
}
return
json
.
Unmarshal
(
bytesValue
,
p
)
}
func
(
p
TaskPrivateData
)
Value
()
(
driver
.
Value
,
error
)
{
if
(
p
==
TaskPrivateData
{})
{
return
nil
,
nil
}
return
json
.
Marshal
(
p
)
}
// SyncTaskQueryParams 用于包含所有搜索条件的结构体,可以根据需求添加更多字段
// SyncTaskQueryParams 用于包含所有搜索条件的结构体,可以根据需求添加更多字段
type
SyncTaskQueryParams
struct
{
type
SyncTaskQueryParams
struct
{
Platform
constant
.
TaskPlatform
Platform
constant
.
TaskPlatform
...
@@ -98,6 +125,12 @@ type SyncTaskQueryParams struct {
...
@@ -98,6 +125,12 @@ type SyncTaskQueryParams struct {
}
}
func
InitTask
(
platform
constant
.
TaskPlatform
,
relayInfo
*
commonRelay
.
RelayInfo
)
*
Task
{
func
InitTask
(
platform
constant
.
TaskPlatform
,
relayInfo
*
commonRelay
.
RelayInfo
)
*
Task
{
properties
:=
Properties
{}
privateData
:=
TaskPrivateData
{}
if
relayInfo
!=
nil
&&
relayInfo
.
ChannelMeta
!=
nil
&&
relayInfo
.
ChannelMeta
.
ChannelType
==
constant
.
ChannelTypeGemini
{
privateData
.
Key
=
relayInfo
.
ChannelMeta
.
ApiKey
}
t
:=
&
Task
{
t
:=
&
Task
{
UserId
:
relayInfo
.
UserId
,
UserId
:
relayInfo
.
UserId
,
Group
:
relayInfo
.
UsingGroup
,
Group
:
relayInfo
.
UsingGroup
,
...
@@ -106,6 +139,8 @@ func InitTask(platform constant.TaskPlatform, relayInfo *commonRelay.RelayInfo)
...
@@ -106,6 +139,8 @@ func InitTask(platform constant.TaskPlatform, relayInfo *commonRelay.RelayInfo)
Progress
:
"0%"
,
Progress
:
"0%"
,
ChannelId
:
relayInfo
.
ChannelId
,
ChannelId
:
relayInfo
.
ChannelId
,
Platform
:
platform
,
Platform
:
platform
,
Properties
:
properties
,
PrivateData
:
privateData
,
}
}
return
t
return
t
}
}
...
...
relay/channel/task/gemini/adaptor.go
View file @
36b71243
...
@@ -7,9 +7,11 @@ import (
...
@@ -7,9 +7,11 @@ import (
"fmt"
"fmt"
"io"
"io"
"net/http"
"net/http"
"regexp"
"strings"
"strings"
"time"
"time"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/model"
...
@@ -248,17 +250,45 @@ func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, e
...
@@ -248,17 +250,45 @@ func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, e
ti
.
Status
=
model
.
TaskStatusSuccess
ti
.
Status
=
model
.
TaskStatusSuccess
ti
.
Progress
=
"100%"
ti
.
Progress
=
"100%"
taskID
:=
encodeLocalTaskID
(
op
.
Name
)
ti
.
TaskID
=
taskID
ti
.
Url
=
fmt
.
Sprintf
(
"%s/v1/videos/%s/content"
,
system_setting
.
ServerAddress
,
taskID
)
// Extract URL from generateVideoResponse if available
// Extract URL from generateVideoResponse if available
if
len
(
op
.
Response
.
GenerateVideoResponse
.
GeneratedSamples
)
>
0
{
if
len
(
op
.
Response
.
GenerateVideoResponse
.
GeneratedSamples
)
>
0
{
if
uri
:=
op
.
Response
.
GenerateVideoResponse
.
GeneratedSamples
[
0
]
.
Video
.
URI
;
uri
!=
""
{
if
uri
:=
op
.
Response
.
GenerateVideoResponse
.
GeneratedSamples
[
0
]
.
Video
.
URI
;
uri
!=
""
{
taskID
:=
encodeLocalTaskID
(
op
.
Name
)
ti
.
RemoteUrl
=
uri
ti
.
Url
=
fmt
.
Sprintf
(
"%s/v1/videos/%s/content?url=%s"
,
system_setting
.
ServerAddress
,
taskID
,
uri
)
}
}
}
}
return
ti
,
nil
return
ti
,
nil
}
}
func
(
a
*
TaskAdaptor
)
ConvertToOpenAIVideo
(
task
*
model
.
Task
)
([]
byte
,
error
)
{
upstreamName
,
err
:=
decodeLocalTaskID
(
task
.
TaskID
)
if
err
!=
nil
{
upstreamName
=
""
}
modelName
:=
extractModelFromOperationName
(
upstreamName
)
if
strings
.
TrimSpace
(
modelName
)
==
""
{
modelName
=
"veo-3.0-generate-001"
}
video
:=
dto
.
NewOpenAIVideo
()
video
.
ID
=
task
.
TaskID
video
.
Model
=
modelName
video
.
Status
=
task
.
Status
.
ToVideoStatus
()
video
.
SetProgressStr
(
task
.
Progress
)
video
.
CreatedAt
=
task
.
CreatedAt
if
task
.
FinishTime
>
0
{
video
.
CompletedAt
=
task
.
FinishTime
}
else
if
task
.
UpdatedAt
>
0
{
video
.
CompletedAt
=
task
.
UpdatedAt
}
return
common
.
Marshal
(
video
)
}
// ============================
// ============================
// helpers
// helpers
// ============================
// ============================
...
@@ -274,3 +304,21 @@ func decodeLocalTaskID(local string) (string, error) {
...
@@ -274,3 +304,21 @@ func decodeLocalTaskID(local string) (string, error) {
}
}
return
string
(
b
),
nil
return
string
(
b
),
nil
}
}
var
modelRe
=
regexp
.
MustCompile
(
`models/([^/]+)/operations/`
)
func
extractModelFromOperationName
(
name
string
)
string
{
if
name
==
""
{
return
""
}
if
m
:=
modelRe
.
FindStringSubmatch
(
name
);
len
(
m
)
==
2
{
return
m
[
1
]
}
if
idx
:=
strings
.
Index
(
name
,
"models/"
);
idx
>=
0
{
s
:=
name
[
idx
+
len
(
"models/"
)
:
]
if
p
:=
strings
.
Index
(
s
,
"/operations/"
);
p
>
0
{
return
s
[
:
p
]
}
}
return
""
}
relay/common/relay_info.go
View file @
36b71243
...
@@ -509,6 +509,7 @@ type TaskInfo struct {
...
@@ -509,6 +509,7 @@ type TaskInfo struct {
Status
string
`json:"status"`
Status
string
`json:"status"`
Reason
string
`json:"reason,omitempty"`
Reason
string
`json:"reason,omitempty"`
Url
string
`json:"url,omitempty"`
Url
string
`json:"url,omitempty"`
RemoteUrl
string
`json:"remote_url,omitempty"`
Progress
string
`json:"progress,omitempty"`
Progress
string
`json:"progress,omitempty"`
CompletionTokens
int
`json:"completion_tokens,omitempty"`
// 用于按倍率计费
CompletionTokens
int
`json:"completion_tokens,omitempty"`
// 用于按倍率计费
TotalTokens
int
`json:"total_tokens,omitempty"`
// 用于按倍率计费
TotalTokens
int
`json:"total_tokens,omitempty"`
// 用于按倍率计费
...
...
relay/relay_task.go
View file @
36b71243
...
@@ -319,7 +319,7 @@ func videoFetchByIDRespBodyBuilder(c *gin.Context) (respBody []byte, taskResp *d
...
@@ -319,7 +319,7 @@ func videoFetchByIDRespBodyBuilder(c *gin.Context) (respBody []byte, taskResp *d
if
err2
!=
nil
{
if
err2
!=
nil
{
return
return
}
}
if
channelModel
.
Type
!=
constant
.
ChannelTypeVertexAi
{
if
channelModel
.
Type
!=
constant
.
ChannelTypeVertexAi
&&
channelModel
.
Type
!=
constant
.
ChannelTypeGemini
{
return
return
}
}
baseURL
:=
constant
.
ChannelBaseURLs
[
channelModel
.
Type
]
baseURL
:=
constant
.
ChannelBaseURLs
[
channelModel
.
Type
]
...
...
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