diff --git a/controller/pricing.go b/controller/pricing.go
index b6537e4..9d1191f 100644
--- a/controller/pricing.go
+++ b/controller/pricing.go
@@ -1,6 +1,7 @@
package controller
import (
+ "github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/service"
"github.com/QuantumNous/new-api/setting/ratio_setting"
@@ -8,6 +9,30 @@ import (
"github.com/gin-gonic/gin"
)
+func filterPricingByUsableGroups(pricing []model.Pricing, usableGroup map[string]string) []model.Pricing {
+ if len(pricing) == 0 {
+ return pricing
+ }
+ if len(usableGroup) == 0 {
+ return []model.Pricing{}
+ }
+
+ filtered := make([]model.Pricing, 0, len(pricing))
+ for _, item := range pricing {
+ if common.StringsContains(item.EnableGroup, "all") {
+ filtered = append(filtered, item)
+ continue
+ }
+ for _, group := range item.EnableGroup {
+ if _, ok := usableGroup[group]; ok {
+ filtered = append(filtered, item)
+ break
+ }
+ }
+ }
+ return filtered
+}
+
func GetPricing(c *gin.Context) {
pricing := model.GetPricing()
userId, exists := c.Get("id")
@@ -31,6 +56,7 @@ func GetPricing(c *gin.Context) {
}
usableGroup = service.GetUserUsableGroups(group)
+ pricing = filterPricingByUsableGroups(pricing, usableGroup)
// check groupRatio contains usableGroup
for group := range ratio_setting.GetGroupRatioCopy() {
if _, ok := usableGroup[group]; !ok {
diff --git a/controller/video_proxy.go b/controller/video_proxy.go
index f1dd2bc..fe8b3fc 100644
--- a/controller/video_proxy.go
+++ b/controller/video_proxy.go
@@ -8,10 +8,12 @@ import (
"net/url"
"time"
+ "github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/logger"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/service"
+ "github.com/QuantumNous/new-api/setting/system_setting"
"github.com/gin-gonic/gin"
)
@@ -109,6 +111,13 @@ func VideoProxy(c *gin.Context) {
return
}
+ fetchSetting := system_setting.GetFetchSetting()
+ if err := common.ValidateURLWithFetchSetting(videoURL, fetchSetting.EnableSSRFProtection, fetchSetting.AllowPrivateIp, fetchSetting.DomainFilterMode, fetchSetting.IpFilterMode, fetchSetting.DomainList, fetchSetting.IpList, fetchSetting.AllowedPorts, fetchSetting.ApplyIPFilterForDomain); err != nil {
+ logger.LogError(c.Request.Context(), fmt.Sprintf("Video URL blocked for task %s: %v", taskID, err))
+ videoProxyError(c, http.StatusForbidden, "server_error", fmt.Sprintf("request blocked: %v", err))
+ return
+ }
+
resp, err := client.Do(req)
if err != nil {
logger.LogError(c.Request.Context(), fmt.Sprintf("Failed to fetch video from %s: %s", videoURL, err.Error()))
diff --git a/dto/gemini.go b/dto/gemini.go
index b97f19e..cc1b502 100644
--- a/dto/gemini.go
+++ b/dto/gemini.go
@@ -121,6 +121,11 @@ func (r *GeminiChatRequest) IsStream(c *gin.Context) bool {
if c.Query("alt") == "sse" {
return true
}
+ // Native Gemini API uses URL action to indicate streaming:
+ // /v1beta/models/{model}:streamGenerateContent
+ if strings.Contains(c.Request.URL.Path, "streamGenerateContent") {
+ return true
+ }
return false
}
diff --git a/dto/openai_request.go b/dto/openai_request.go
index c0a69a3..ef6dbed 100644
--- a/dto/openai_request.go
+++ b/dto/openai_request.go
@@ -387,7 +387,7 @@ func (m *MediaContent) GetVideoUrl() *MessageVideoUrl {
type MessageImageUrl struct {
Url string `json:"url"`
- Detail string `json:"detail"`
+ Detail string `json:"detail,omitempty"`
MimeType string
}
diff --git a/relay/channel/claude/relay-claude.go b/relay/channel/claude/relay-claude.go
index 069c784..be2e2a8 100644
--- a/relay/channel/claude/relay-claude.go
+++ b/relay/channel/claude/relay-claude.go
@@ -743,7 +743,16 @@ func HandleStreamFinalResponse(c *gin.Context, info *relaycommon.RelayInfo, clau
if common.DebugEnabled {
common.SysLog("claude response usage is not complete, maybe upstream error")
}
- claudeInfo.Usage = service.ResponseText2Usage(c, claudeInfo.ResponseText.String(), info.UpstreamModelName, claudeInfo.Usage.PromptTokens)
+ // 只补缺失字段,不整份覆盖——保留 message_start 已拿到的 cache 字段
+ fallback := service.ResponseText2Usage(c, claudeInfo.ResponseText.String(), info.UpstreamModelName, info.GetEstimatePromptTokens())
+ if claudeInfo.Usage.CompletionTokens == 0 ||
+ (!claudeInfo.Done && fallback.CompletionTokens > claudeInfo.Usage.CompletionTokens) {
+ claudeInfo.Usage.CompletionTokens = fallback.CompletionTokens
+ }
+ if claudeInfo.Usage.PromptTokens == 0 {
+ claudeInfo.Usage.PromptTokens = fallback.PromptTokens
+ }
+ claudeInfo.Usage.TotalTokens = claudeInfo.Usage.PromptTokens + claudeInfo.Usage.CompletionTokens
}
if info.RelayFormat == types.RelayFormatClaude {
diff --git a/relay/channel/task/ali/adaptor.go b/relay/channel/task/ali/adaptor.go
index f698fc9..5b6b01d 100644
--- a/relay/channel/task/ali/adaptor.go
+++ b/relay/channel/task/ali/adaptor.go
@@ -80,9 +80,9 @@ type AliVideoOutput struct {
// AliUsage 使用统计
type AliUsage struct {
- Duration int `json:"duration,omitempty"`
- VideoCount int `json:"video_count,omitempty"`
- SR int `json:"SR,omitempty"`
+ Duration dto.IntValue `json:"duration,omitempty"`
+ VideoCount dto.IntValue `json:"video_count,omitempty"`
+ SR dto.IntValue `json:"SR,omitempty"`
}
type AliMetadata struct {
diff --git a/relay/channel/zhipu_4v/adaptor.go b/relay/channel/zhipu_4v/adaptor.go
index 597c485..6e547f6 100644
--- a/relay/channel/zhipu_4v/adaptor.go
+++ b/relay/channel/zhipu_4v/adaptor.go
@@ -63,6 +63,9 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
}
return fmt.Sprintf("%s/api/paas/v4/embeddings", baseURL), nil
case relayconstant.RelayModeImagesGenerations:
+ if hasSpecialPlan && specialPlan.OpenAIBaseURL != "" {
+ return fmt.Sprintf("%s/images/generations", specialPlan.OpenAIBaseURL), nil
+ }
return fmt.Sprintf("%s/api/paas/v4/images/generations", baseURL), nil
default:
if hasSpecialPlan && specialPlan.OpenAIBaseURL != "" {
diff --git a/relay/mjproxy_handler.go b/relay/mjproxy_handler.go
index 8e7c61e..63bc907 100644
--- a/relay/mjproxy_handler.go
+++ b/relay/mjproxy_handler.go
@@ -49,6 +49,13 @@ func RelayMidjourneyImage(c *gin.Context) {
if httpClient == nil {
httpClient = service.GetHttpClient()
}
+ fetchSetting := system_setting.GetFetchSetting()
+ if err := common.ValidateURLWithFetchSetting(midjourneyTask.ImageUrl, fetchSetting.EnableSSRFProtection, fetchSetting.AllowPrivateIp, fetchSetting.DomainFilterMode, fetchSetting.IpFilterMode, fetchSetting.DomainList, fetchSetting.IpList, fetchSetting.AllowedPorts, fetchSetting.ApplyIPFilterForDomain); err != nil {
+ c.JSON(http.StatusForbidden, gin.H{
+ "error": fmt.Sprintf("request blocked: %v", err),
+ })
+ return
+ }
resp, err := httpClient.Get(midjourneyTask.ImageUrl)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
diff --git a/router/api-router.go b/router/api-router.go
index 5b80d5c..9e87f8d 100644
--- a/router/api-router.go
+++ b/router/api-router.go
@@ -246,7 +246,7 @@ func SetApiRouter(router *gin.Engine) {
channelRoute.POST("/batch", controller.DeleteChannelBatch)
channelRoute.POST("/fix", controller.FixChannelsAbilities)
channelRoute.GET("/fetch_models/:id", controller.FetchUpstreamModels)
- channelRoute.POST("/fetch_models", controller.FetchModels)
+ channelRoute.POST("/fetch_models", middleware.RootAuth(), controller.FetchModels)
channelRoute.POST("/codex/oauth/start", controller.StartCodexOAuth)
channelRoute.POST("/codex/oauth/complete", controller.CompleteCodexOAuth)
channelRoute.POST("/:id/codex/oauth/start", controller.StartCodexOAuthForChannel)
@@ -396,6 +396,7 @@ func SetApiRouter(router *gin.Engine) {
syncRoute.POST("/user/create", controller.ReceiveSyncedUserCreate)
syncRoute.POST("/quota/update", controller.ReceiveQuotaUpdate)
syncRoute.POST("/quota/query", controller.QueryUserQuota)
+ syncRoute.POST("/quota/batch-query", controller.BatchQueryUserQuota)
syncRoute.POST("/quota/batch-deduct", controller.BatchDeductQuota)
syncRoute.GET("/config", controller.GetSyncConfig)
}
diff --git a/setting/ratio_setting/model_ratio.go b/setting/ratio_setting/model_ratio.go
index 62a0010..6014f3f 100644
--- a/setting/ratio_setting/model_ratio.go
+++ b/setting/ratio_setting/model_ratio.go
@@ -357,6 +357,11 @@ func UpdateModelPriceByJSONString(jsonStr string) error {
func GetModelPrice(name string, printErr bool) (float64, bool) {
name = FormatMatchingModelName(name)
+ // 优先匹配精确模型名称
+ if price, ok := modelPriceMap.Get(name); ok {
+ return price, true
+ }
+
if strings.HasSuffix(name, CompactModelSuffix) {
price, ok := modelPriceMap.Get(CompactWildcardModelKey)
if !ok {
diff --git a/setting/system_setting/fetch_setting.go b/setting/system_setting/fetch_setting.go
index 0786961..c71be03 100644
--- a/setting/system_setting/fetch_setting.go
+++ b/setting/system_setting/fetch_setting.go
@@ -21,7 +21,7 @@ var defaultFetchSetting = FetchSetting{
DomainList: []string{},
IpList: []string{},
AllowedPorts: []string{"80", "443", "8080", "8443"},
- ApplyIPFilterForDomain: false,
+ ApplyIPFilterForDomain: true,
}
func init() {
diff --git a/web/src/components/common/DocumentRenderer/index.jsx b/web/src/components/common/DocumentRenderer/index.jsx
index 68e868c..8c15c07 100644
--- a/web/src/components/common/DocumentRenderer/index.jsx
+++ b/web/src/components/common/DocumentRenderer/index.jsx
@@ -17,7 +17,7 @@ along with this program. If not, see