Skip to content
Closed

1 #3475

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
680271d
feat(playground): add video size/duration/quality selectors
LJR199887 Mar 27, 2026
988b297
Merge pull request #1 from LJR199887/codex/find-project-purpose
LJR199887 Mar 27, 2026
9db210d
fix(playground): route video models to video endpoint
LJR199887 Mar 28, 2026
f5b1a79
Merge branch 'QuantumNous:main' into main
LJR199887 Mar 28, 2026
6658556
fix(playground): always route video models to video endpoint
LJR199887 Mar 28, 2026
d31fbb6
Merge branch 'main' into codex/find-project-purpose-3vdzr3
LJR199887 Mar 28, 2026
8446231
Merge pull request #2 from LJR199887/codex/find-project-purpose-3vdzr3
LJR199887 Mar 28, 2026
90063cb
fix(playground): use user-authenticated pg video generation endpoint
LJR199887 Mar 28, 2026
2186b57
Merge branch 'main' into codex/find-project-purpose-8t1iot
LJR199887 Mar 28, 2026
8e89b09
Merge pull request #3 from LJR199887/codex/find-project-purpose-8t1iot
LJR199887 Mar 28, 2026
989f667
feat(playground): show video content URL after task creation
LJR199887 Mar 28, 2026
37f5126
Merge branch 'main' into codex/find-project-purpose
LJR199887 Mar 28, 2026
e20c678
Merge pull request #4 from LJR199887/codex/find-project-purpose
LJR199887 Mar 28, 2026
629be09
Revert "Add playground video generation endpoints and UI; wire video …
LJR199887 Mar 28, 2026
b69a41e
Merge pull request #5 from LJR199887/revert-4-codex/find-project-purpose
LJR199887 Mar 28, 2026
8e19212
fix grok video url response
LJR199887 Mar 28, 2026
2933917
avoid duplicate video preview
LJR199887 Mar 28, 2026
df6cb95
add grok video preset options
LJR199887 Mar 28, 2026
37f727c
fix grok video quality request handling
LJR199887 Mar 28, 2026
b1b647d
fix grok 720p video request aliases
LJR199887 Mar 28, 2026
b1a56ec
Fix grok imagine playground image routing
LJR199887 Mar 28, 2026
8671548
Fix playground image relay mode
LJR199887 Mar 28, 2026
3ca517d
Handle empty prompt for grok imagine edit
LJR199887 Mar 28, 2026
ca554ba
Fix grok imagine video endpoint display
LJR199887 Mar 28, 2026
5a65dc4
Merge pull request #6 from LJR199887/codex/fix-grok-video-url
LJR199887 Mar 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions common/endpoint_defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ var defaultEndpointInfoMap = map[constant.EndpointType]EndpointInfo{
constant.EndpointTypeGemini: {Path: "/v1beta/models/{model}:generateContent", Method: "POST"},
constant.EndpointTypeJinaRerank: {Path: "/v1/rerank", Method: "POST"},
constant.EndpointTypeImageGeneration: {Path: "/v1/images/generations", Method: "POST"},
constant.EndpointTypeImageEdit: {Path: "/v1/images/edits", Method: "POST"},
constant.EndpointTypeEmbeddings: {Path: "/v1/embeddings", Method: "POST"},
constant.EndpointTypeOpenAIVideo: {Path: "/v1/video/generations", Method: "POST"},
}

// GetDefaultEndpointInfo 返回指定端点类型的默认信息以及是否存在
Expand Down
4 changes: 3 additions & 1 deletion common/endpoint_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ func GetEndpointTypesByChannelType(channelType int, modelName string) []constant
endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAI}
}
}
if IsImageGenerationModel(modelName) {
if IsImageEditModel(modelName) {
endpointTypes = append([]constant.EndpointType{constant.EndpointTypeImageEdit}, endpointTypes...)
} else if IsImageGenerationModel(modelName) {
// add to first
endpointTypes = append([]constant.EndpointType{constant.EndpointTypeImageGeneration}, endpointTypes...)
}
Expand Down
53 changes: 53 additions & 0 deletions common/endpoint_type_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package common

import (
"testing"

"github.com/QuantumNous/new-api/constant"
)

func TestGetEndpointTypesByChannelTypeRecognizesGrokImagineImageModels(t *testing.T) {
tests := []struct {
name string
channel int
model string
expected constant.EndpointType
}{
{
name: "grok imagine 1.0 generation",
channel: constant.ChannelTypeXai,
model: "grok-imagine-1.0",
expected: constant.EndpointTypeImageGeneration,
},
{
name: "grok imagine 1.0 fast generation",
channel: constant.ChannelTypeXai,
model: "grok-imagine-1.0-fast",
expected: constant.EndpointTypeImageGeneration,
},
{
name: "grok imagine 1.0 edit",
channel: constant.ChannelTypeXai,
model: "grok-imagine-1.0-edit",
expected: constant.EndpointTypeImageEdit,
},
{
name: "grok imagine video stays video",
channel: constant.ChannelTypeSora,
model: "grok-imagine-1.0-video",
expected: constant.EndpointTypeOpenAIVideo,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := GetEndpointTypesByChannelType(tt.channel, tt.model)
if len(got) == 0 {
t.Fatalf("expected endpoint types for %s", tt.model)
}
if got[0] != tt.expected {
t.Fatalf("expected first endpoint %s, got %s", tt.expected, got[0])
}
})
}
}
24 changes: 24 additions & 0 deletions common/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,15 @@ var (
"dall-e-3",
"dall-e-2",
"gpt-image-1",
"exact:grok-imagine-1.0",
"exact:grok-imagine-1.0-fast",
"prefix:imagen-",
"flux-",
"flux.1-",
}
ImageEditModels = []string{
"exact:grok-imagine-1.0-edit",
}
OpenAITextModels = []string{
"gpt-",
"o1",
Expand All @@ -38,6 +43,25 @@ func IsOpenAIResponseOnlyModel(modelName string) bool {
func IsImageGenerationModel(modelName string) bool {
modelName = strings.ToLower(modelName)
for _, m := range ImageGenerationModels {
if strings.HasPrefix(m, "exact:") && modelName == strings.TrimPrefix(m, "exact:") {
return true
}
if strings.Contains(modelName, m) {
return true
}
if strings.HasPrefix(m, "prefix:") && strings.HasPrefix(modelName, strings.TrimPrefix(m, "prefix:")) {
return true
}
}
return false
}

func IsImageEditModel(modelName string) bool {
modelName = strings.ToLower(modelName)
for _, m := range ImageEditModels {
if strings.HasPrefix(m, "exact:") && modelName == strings.TrimPrefix(m, "exact:") {
return true
}
if strings.Contains(modelName, m) {
return true
}
Expand Down
1 change: 1 addition & 0 deletions constant/endpoint_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const (
EndpointTypeGemini EndpointType = "gemini"
EndpointTypeJinaRerank EndpointType = "jina-rerank"
EndpointTypeImageGeneration EndpointType = "image-generation"
EndpointTypeImageEdit EndpointType = "image-edit"
EndpointTypeEmbeddings EndpointType = "embeddings"
EndpointTypeOpenAIVideo EndpointType = "openai-video"
//EndpointTypeMidjourney EndpointType = "midjourney-proxy"
Expand Down
87 changes: 78 additions & 9 deletions controller/playground.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,22 +35,91 @@ func Playground(c *gin.Context) {
return
}

userId := c.GetInt("id")
if newAPIError = setupPlaygroundTokenContext(c, fmt.Sprintf("playground-%s", relayInfo.UsingGroup), relayInfo.UsingGroup); newAPIError != nil {
return
}

Relay(c, types.RelayFormatOpenAI)
}

// Write user context to ensure acceptUnsetRatio is available
func PlaygroundVideoSubmit(c *gin.Context) {
var newAPIError *types.NewAPIError
defer func() {
if newAPIError != nil {
c.JSON(newAPIError.StatusCode, gin.H{
"error": newAPIError.ToOpenAIError(),
})
}
}()
if newAPIError = setupPlaygroundTokenContext(c, "playground-video", c.GetString("group")); newAPIError != nil {
return
}
RelayTask(c)
}

func PlaygroundImageGenerations(c *gin.Context) {
var newAPIError *types.NewAPIError
defer func() {
if newAPIError != nil {
c.JSON(newAPIError.StatusCode, gin.H{
"error": newAPIError.ToOpenAIError(),
})
}
}()
if newAPIError = setupPlaygroundTokenContext(c, "playground-image", c.GetString("group")); newAPIError != nil {
return
}
Relay(c, types.RelayFormatOpenAIImage)
}

func PlaygroundImageEdits(c *gin.Context) {
var newAPIError *types.NewAPIError
defer func() {
if newAPIError != nil {
c.JSON(newAPIError.StatusCode, gin.H{
"error": newAPIError.ToOpenAIError(),
})
}
}()
if newAPIError = setupPlaygroundTokenContext(c, "playground-image-edit", c.GetString("group")); newAPIError != nil {
return
}
Relay(c, types.RelayFormatOpenAIImage)
}

func PlaygroundVideoFetch(c *gin.Context) {
var newAPIError *types.NewAPIError
defer func() {
if newAPIError != nil {
c.JSON(newAPIError.StatusCode, gin.H{
"error": newAPIError.ToOpenAIError(),
})
}
}()
if newAPIError = setupPlaygroundTokenContext(c, "playground-video-fetch", c.GetString("group")); newAPIError != nil {
return
}
RelayTaskFetch(c)
}
Comment on lines +45 to +103

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reject access-token flows in video playground handlers for parity with Playground.

PlaygroundVideoSubmit and PlaygroundVideoFetch currently miss the use_access_token guard that Playground enforces. This introduces inconsistent auth behavior for /pg/video/*.

🔧 Suggested fix
 func PlaygroundVideoSubmit(c *gin.Context) {
 	var newAPIError *types.NewAPIError
 	defer func() {
 		if newAPIError != nil {
 			c.JSON(newAPIError.StatusCode, gin.H{
 				"error": newAPIError.ToOpenAIError(),
 			})
 		}
 	}()
+	if c.GetBool("use_access_token") {
+		newAPIError = types.NewError(errors.New("暂不支持使用 access token"), types.ErrorCodeAccessDenied, types.ErrOptionWithSkipRetry())
+		return
+	}
 	if newAPIError = setupPlaygroundTokenContext(c, "playground-video", c.GetString("group")); newAPIError != nil {
 		return
 	}
 	RelayTask(c)
 }
 
 func PlaygroundVideoFetch(c *gin.Context) {
 	var newAPIError *types.NewAPIError
 	defer func() {
 		if newAPIError != nil {
 			c.JSON(newAPIError.StatusCode, gin.H{
 				"error": newAPIError.ToOpenAIError(),
 			})
 		}
 	}()
+	if c.GetBool("use_access_token") {
+		newAPIError = types.NewError(errors.New("暂不支持使用 access token"), types.ErrorCodeAccessDenied, types.ErrOptionWithSkipRetry())
+		return
+	}
 	if newAPIError = setupPlaygroundTokenContext(c, "playground-video-fetch", c.GetString("group")); newAPIError != nil {
 		return
 	}
 	RelayTaskFetch(c)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func PlaygroundVideoSubmit(c *gin.Context) {
var newAPIError *types.NewAPIError
defer func() {
if newAPIError != nil {
c.JSON(newAPIError.StatusCode, gin.H{
"error": newAPIError.ToOpenAIError(),
})
}
}()
if newAPIError = setupPlaygroundTokenContext(c, "playground-video", c.GetString("group")); newAPIError != nil {
return
}
RelayTask(c)
}
func PlaygroundVideoFetch(c *gin.Context) {
var newAPIError *types.NewAPIError
defer func() {
if newAPIError != nil {
c.JSON(newAPIError.StatusCode, gin.H{
"error": newAPIError.ToOpenAIError(),
})
}
}()
if newAPIError = setupPlaygroundTokenContext(c, "playground-video-fetch", c.GetString("group")); newAPIError != nil {
return
}
RelayTaskFetch(c)
}
func PlaygroundVideoSubmit(c *gin.Context) {
var newAPIError *types.NewAPIError
defer func() {
if newAPIError != nil {
c.JSON(newAPIError.StatusCode, gin.H{
"error": newAPIError.ToOpenAIError(),
})
}
}()
if c.GetBool("use_access_token") {
newAPIError = types.NewError(errors.New("暂不支持使用 access token"), types.ErrorCodeAccessDenied, types.ErrOptionWithSkipRetry())
return
}
if newAPIError = setupPlaygroundTokenContext(c, "playground-video", c.GetString("group")); newAPIError != nil {
return
}
RelayTask(c)
}
func PlaygroundVideoFetch(c *gin.Context) {
var newAPIError *types.NewAPIError
defer func() {
if newAPIError != nil {
c.JSON(newAPIError.StatusCode, gin.H{
"error": newAPIError.ToOpenAIError(),
})
}
}()
if c.GetBool("use_access_token") {
newAPIError = types.NewError(errors.New("暂不支持使用 access token"), types.ErrorCodeAccessDenied, types.ErrOptionWithSkipRetry())
return
}
if newAPIError = setupPlaygroundTokenContext(c, "playground-video-fetch", c.GetString("group")); newAPIError != nil {
return
}
RelayTaskFetch(c)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/playground.go` around lines 45 - 73, Add the same "reject
access-token flows" guard to PlaygroundVideoSubmit and PlaygroundVideoFetch as
in Playground: after calling setupPlaygroundTokenContext (and before
RelayTask/RelayTaskFetch), check the request for the use_access_token flag (the
same source Playground uses, e.g., query/header/context key "use_access_token")
and if it's set/true return a NewAPIError (same type/format Playground returns)
so the handlers reject access-token flows consistently.


func setupPlaygroundTokenContext(c *gin.Context, tokenName string, tokenGroup string) *types.NewAPIError {
userId := c.GetInt("id")
userCache, err := model.GetUserCache(userId)
if err != nil {
newAPIError = types.NewError(err, types.ErrorCodeQueryDataError, types.ErrOptionWithSkipRetry())
return
return types.NewError(err, types.ErrorCodeQueryDataError, types.ErrOptionWithSkipRetry())
}
userCache.WriteContext(c)

if tokenGroup == "" {
tokenGroup = c.GetString("group")
}
if tokenGroup == "" {
tokenGroup = userCache.Group
}
tempToken := &model.Token{
UserId: userId,
Name: fmt.Sprintf("playground-%s", relayInfo.UsingGroup),
Group: relayInfo.UsingGroup,
Name: tokenName,
Group: tokenGroup,
}
_ = middleware.SetupContextForToken(c, tempToken)

Relay(c, types.RelayFormatOpenAI)
return nil
}
37 changes: 37 additions & 0 deletions controller/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/QuantumNous/new-api/middleware"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/relay"
taskcommon "github.com/QuantumNous/new-api/relay/channel/task/taskcommon"
relaycommon "github.com/QuantumNous/new-api/relay/common"
relayconstant "github.com/QuantumNous/new-api/relay/constant"
"github.com/QuantumNous/new-api/relay/helper"
Expand Down Expand Up @@ -586,6 +587,42 @@ func RelayTask(c *gin.Context) {
task.Quota = result.Quota
task.Data = result.TaskData
task.Action = relayInfo.Action
if adaptor := relay.GetTaskAdaptor(result.Platform); adaptor != nil && len(result.TaskData) > 0 {
if taskInfo, err := adaptor.ParseTaskResult(result.TaskData); err == nil && taskInfo != nil && taskInfo.Status != "" {
now := time.Now().Unix()
task.Status = model.TaskStatus(taskInfo.Status)
switch task.Status {
case model.TaskStatusSubmitted:
task.Progress = taskcommon.ProgressSubmitted
case model.TaskStatusQueued:
task.Progress = taskcommon.ProgressQueued
case model.TaskStatusInProgress:
task.Progress = taskcommon.ProgressInProgress
if task.StartTime == 0 {
task.StartTime = now
}
case model.TaskStatusSuccess:
task.Progress = taskcommon.ProgressComplete
if task.StartTime == 0 {
task.StartTime = now
}
if task.FinishTime == 0 {
task.FinishTime = now
}
task.PrivateData.ResultURL = taskInfo.Url
case model.TaskStatusFailure:
task.Progress = taskcommon.ProgressComplete
if task.FinishTime == 0 {
task.FinishTime = now
}
task.FailReason = taskInfo.Reason
task.PrivateData.ResultURL = taskInfo.Url
}
if taskInfo.Progress != "" {
task.Progress = taskInfo.Progress
}
}
}
if insertErr := task.Insert(); insertErr != nil {
common.SysError("insert task error: " + insertErr.Error())
}
Expand Down
2 changes: 2 additions & 0 deletions dto/openai_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ type GeneralOpenAIRequest struct {
Input any `json:"input,omitempty"`
Instruction string `json:"instruction,omitempty"`
Size string `json:"size,omitempty"`
Seconds *string `json:"seconds,omitempty"`
Quality *string `json:"quality,omitempty"`
Functions json.RawMessage `json:"functions,omitempty"`
FrequencyPenalty *float64 `json:"frequency_penalty,omitempty"`
PresencePenalty *float64 `json:"presence_penalty,omitempty"`
Expand Down
4 changes: 2 additions & 2 deletions middleware/distributor.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ func Distribute() func(c *gin.Context) {
}
var selectGroup string
usingGroup := common.GetContextKeyString(c, constant.ContextKeyUsingGroup)
// check path is /pg/chat/completions
if strings.HasPrefix(c.Request.URL.Path, "/pg/chat/completions") {
// playground requests may override group in body
if strings.HasPrefix(c.Request.URL.Path, "/pg/") {
Comment on lines +84 to +85

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

/pg/* expansion exposes missing model/relay parsing for non-chat playground routes.

After broadening to /pg/, requests like /pg/images/edits and /pg/video/generations still rely on getModelRequest, which currently only has explicit /pg/chat/completions handling. This can leave modelRequest.Model empty or shouldSelectChannel incorrect for new playground routes.

🔧 Suggested direction (normalize path once for getModelRequest decisions)
func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
+	pathForMatch := c.Request.URL.Path
+	if strings.HasPrefix(pathForMatch, "/pg/") {
+		pathForMatch = "/v1" + strings.TrimPrefix(pathForMatch, "/pg")
+	}
 	var modelRequest ModelRequest
 	shouldSelectChannel := true
 	var err error
-	if strings.Contains(c.Request.URL.Path, "/v1/video/generations") {
+	if strings.Contains(pathForMatch, "/v1/video/generations") {
 		...
 	}
-	if strings.HasPrefix(c.Request.URL.Path, "/v1/images/generations") {
+	if strings.HasPrefix(pathForMatch, "/v1/images/generations") {
 		...
-	} else if strings.HasPrefix(c.Request.URL.Path, "/v1/images/edits") {
+	} else if strings.HasPrefix(pathForMatch, "/v1/images/edits") {
 		...
 	}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@middleware/distributor.go` around lines 84 - 85, Requests under /pg/* can hit
getModelRequest which only handles /pg/chat/completions, leaving
modelRequest.Model empty and shouldSelectChannel incorrect for routes like
/pg/images/* or /pg/video/*; update the middleware so when
strings.HasPrefix(c.Request.URL.Path, "/pg/") you normalize the path (or add
explicit cases) before calling getModelRequest: either map known playground
prefixes (e.g., "/pg/chat", "/pg/images", "/pg/video") to appropriate normalized
paths or set the relevant fields on the modelRequest (Model/Relay) so
getModelRequest and shouldSelectChannel see the correct route context; touch
distributor.go around the existing /pg/ branch and adjust
getModelRequest/shouldSelectChannel logic to accept the normalized path or
additional /pg/* cases.

playgroundRequest := &dto.PlayGroundRequest{}
err = common.UnmarshalBodyReusable(c, playgroundRequest)
if err != nil {
Expand Down
Loading