去掉1500字数限制

This commit is contained in:
2026-07-02 20:54:52 +08:00
parent 7a168a1dd8
commit c5a059df6c
4 changed files with 26 additions and 38 deletions
@@ -70,7 +70,7 @@ func (h *UserGenerationHandler) Generate(c *gin.Context) {
switch { switch {
case errors.Is(err, service.ErrUnknownModel): case errors.Is(err, service.ErrUnknownModel):
c.JSON(http.StatusNotFound, gin.H{"detail": err.Error()}) c.JSON(http.StatusNotFound, gin.H{"detail": err.Error()})
case errors.Is(err, service.ErrUnsupportedParams), errors.Is(err, service.ErrPromptTooLong): case errors.Is(err, service.ErrUnsupportedParams):
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
case errors.Is(err, service.ErrInsufficientFunds): case errors.Is(err, service.ErrInsufficientFunds):
c.JSON(http.StatusPaymentRequired, gin.H{"detail": "积分不足"}) c.JSON(http.StatusPaymentRequired, gin.H{"detail": "积分不足"})
@@ -132,7 +132,7 @@ func (h *UserGenerationHandler) Test(c *gin.Context) {
switch { switch {
case errors.Is(err, service.ErrUnknownModel): case errors.Is(err, service.ErrUnknownModel):
c.JSON(http.StatusNotFound, gin.H{"detail": err.Error()}) c.JSON(http.StatusNotFound, gin.H{"detail": err.Error()})
case errors.Is(err, service.ErrUnsupportedParams), errors.Is(err, service.ErrPromptTooLong): case errors.Is(err, service.ErrUnsupportedParams):
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
case errors.Is(err, service.ErrProviderQuota): case errors.Is(err, service.ErrProviderQuota):
c.JSON(http.StatusTooManyRequests, gin.H{"detail": err.Error()}) c.JSON(http.StatusTooManyRequests, gin.H{"detail": err.Error()})
@@ -356,17 +356,17 @@ func (h *UserGenerationHandler) Models(c *gin.Context) {
func (h *UserGenerationHandler) catalogEntries(c *gin.Context) ([]gin.H, error) { func (h *UserGenerationHandler) catalogEntries(c *gin.Context) ([]gin.H, error) {
items := []gin.H{ items := []gin.H{
{ {
"id": "gpt-image-2", "id": "gpt-image-2",
"provider": "chatgpt", "provider": "chatgpt",
"type": "image", "type": "image",
// ChatGPT web backend only reliably produces 1K and honors a limited // ChatGPT web backend only reliably produces 1K and honors a limited
// ratio set; size params are advisory prompt hints. Mirrors the Python // ratio set; size params are advisory prompt hints. Mirrors the Python
// reference (providers/chatgpt/provider.py) — do not offer 2K/4K. // reference (providers/chatgpt/provider.py) — do not offer 2K/4K.
"ratios": []string{"1:1", "16:9", "9:16", "4:3", "3:4"}, "ratios": []string{"1:1", "16:9", "9:16", "4:3", "3:4"},
"resolutions": []string{"1K"}, "resolutions": []string{"1K"},
"image_to_image": true, "image_to_image": true,
"max_reference_images": 3, "max_reference_images": 3,
"description": "ChatGPT image generation", "description": "ChatGPT image generation",
}, },
{ {
"id": "firefly-gpt-image-2", "id": "firefly-gpt-image-2",
@@ -521,9 +521,9 @@ func (h *UserGenerationHandler) catalogEntries(c *gin.Context) ([]gin.H, error)
func (h *UserGenerationHandler) publicModels() ([]gin.H, error) { func (h *UserGenerationHandler) publicModels() ([]gin.H, error) {
items := []gin.H{ items := []gin.H{
{ {
"id": "gpt-image-2", "id": "gpt-image-2",
"provider": "chatgpt", "provider": "chatgpt",
"kind": "image", "kind": "image",
// See catalogEntries — ChatGPT only reliably does 1K and a limited // See catalogEntries — ChatGPT only reliably does 1K and a limited
// ratio set; matches the Python reference. Keep both lists in sync. // ratio set; matches the Python reference. Keep both lists in sync.
"ratios": []string{"1:1", "16:9", "9:16", "4:3", "3:4"}, "ratios": []string{"1:1", "16:9", "9:16", "4:3", "3:4"},
+1 -1
View File
@@ -316,7 +316,7 @@ func (h *V1Handler) writeV1Error(c *gin.Context, err error, payload map[string]a
switch { switch {
case errors.Is(err, service.ErrUnknownModel): case errors.Is(err, service.ErrUnknownModel):
c.JSON(http.StatusNotFound, gin.H{"detail": err.Error()}) c.JSON(http.StatusNotFound, gin.H{"detail": err.Error()})
case errors.Is(err, service.ErrUnsupportedParams), errors.Is(err, service.ErrPromptTooLong): case errors.Is(err, service.ErrUnsupportedParams):
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
case errors.Is(err, service.ErrInsufficientFunds): case errors.Is(err, service.ErrInsufficientFunds):
c.JSON(http.StatusPaymentRequired, gin.H{"detail": err.Error()}) c.JSON(http.StatusPaymentRequired, gin.H{"detail": err.Error()})
+11 -23
View File
@@ -14,7 +14,6 @@ import (
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"unicode/utf8"
"strconv" "strconv"
@@ -38,7 +37,6 @@ var (
ErrInvalidAPIKey = errors.New("invalid api key") ErrInvalidAPIKey = errors.New("invalid api key")
ErrUnknownModel = errors.New("unknown model") ErrUnknownModel = errors.New("unknown model")
ErrUnsupportedParams = errors.New("unsupported or unpriced parameters for this model") ErrUnsupportedParams = errors.New("unsupported or unpriced parameters for this model")
ErrPromptTooLong = errors.New("prompt too long (max 1500 characters)")
ErrInsufficientFunds = errors.New("insufficient credits") ErrInsufficientFunds = errors.New("insufficient credits")
ErrGenerationPending = errors.New("generation executor not implemented yet") ErrGenerationPending = errors.New("generation executor not implemented yet")
ErrProviderAuth = errors.New("provider token invalid or expired") ErrProviderAuth = errors.New("provider token invalid or expired")
@@ -59,10 +57,6 @@ var (
ErrVideoNotReady = errors.New("video is not ready yet") ErrVideoNotReady = errors.New("video is not ready yet")
) )
// maxPromptRunes caps prompt length (counted as characters, so Chinese = 1),
// matching the 画图台 counter. Enforced for both 画图台 and API-key calls.
const maxPromptRunes = 1500
// maxReferenceImageBytes bounds a single decoded reference image. 8 MB // maxReferenceImageBytes bounds a single decoded reference image. 8 MB
// comfortably covers real photos/screenshots; anything larger is almost // comfortably covers real photos/screenshots; anything larger is almost
// certainly abuse or a mistake. Mirrors Python core/refs.py. // certainly abuse or a mistake. Mirrors Python core/refs.py.
@@ -88,7 +82,7 @@ type V1Service struct {
// refresh re-mints an Adobe access token from its cookie when a request hits a // refresh re-mints an Adobe access token from its cookie when a request hits a
// 401 mid-flight (set via SetRefresh — wired after construction to avoid an // 401 mid-flight (set via SetRefresh — wired after construction to avoid an
// init cycle). nil for deployments without cookie refresh. // init cycle). nil for deployments without cookie refresh.
refresh *RefreshProfileService refresh *RefreshProfileService
// tokenCursors holds one strict round-robin cursor per pool (key: pool name, // tokenCursors holds one strict round-robin cursor per pool (key: pool name,
// value: *uint64). Each pick advances the pool's cursor by one so accounts // value: *uint64). Each pick advances the pool's cursor by one so accounts
@@ -188,9 +182,9 @@ type APIPrincipal struct {
} }
type V1ImageRequest struct { type V1ImageRequest struct {
Model string Model string
Prompt string Prompt string
Size string Size string
// Quality is OpenAI's image quality (low|medium|high|auto). For our tiered // Quality is OpenAI's image quality (low|medium|high|auto). For our tiered
// models it selects the resolution (low→1K, medium→2K, high→4K, auto→default), // models it selects the resolution (low→1K, medium→2K, high→4K, auto→default),
// clamped to whatever tiers the model actually prices. Only used when // clamped to whatever tiers the model actually prices. Only used when
@@ -945,9 +939,6 @@ func (s *V1Service) prepareImage(ctx context.Context, principal *APIPrincipal, i
if modelID == "" || prompt == "" { if modelID == "" || prompt == "" {
return nil, "", "", 0, errors.New("model and prompt required") return nil, "", "", 0, errors.New("model and prompt required")
} }
if utf8.RuneCountInString(prompt) > maxPromptRunes {
return nil, "", "", 0, ErrPromptTooLong
}
modelItem, err := s.models.Get(ctx, modelID) modelItem, err := s.models.Get(ctx, modelID)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -1011,9 +1002,6 @@ func (s *V1Service) prepareVideo(ctx context.Context, principal *APIPrincipal, i
if modelID == "" || prompt == "" { if modelID == "" || prompt == "" {
return nil, "", "", "", 0, errors.New("model and prompt required") return nil, "", "", "", 0, errors.New("model and prompt required")
} }
if utf8.RuneCountInString(prompt) > maxPromptRunes {
return nil, "", "", "", 0, ErrPromptTooLong
}
if duration == "" { if duration == "" {
return nil, "", "", "", 0, errors.New("duration required") return nil, "", "", "", 0, errors.New("duration required")
} }
@@ -1278,13 +1266,13 @@ const maxTempDeadAccounts = 3
// fresh token; if it still auth-fails (or there's nothing to refresh, e.g. // fresh token; if it still auth-fails (or there's nothing to refresh, e.g.
// chatgpt's JWT IS the credential), mark the account and fail over. // chatgpt's JWT IS the credential), mark the account and fail over.
// - 上游临时 temporary → behavior depends on tempFailover: // - 上游临时 temporary → behavior depends on tempFailover:
// tempFailover=false (default): retry the SAME account up to // - tempFailover=false (default): retry the SAME account up to
// maxSameAccountAttempts times (not counted); if still failing, STOP // maxSameAccountAttempts times (not counted); if still failing, STOP
// (no fan-out — an upstream-wide blip fails identically everywhere). // (no fan-out — an upstream-wide blip fails identically everywhere).
// tempFailover=true (adobe): fail over to the next account WITHOUT // - tempFailover=true (adobe): fail over to the next account WITHOUT
// penalizing this one (rate-limit/overload isn't the account's fault), // penalizing this one (rate-limit/overload isn't the account's fault),
// capped at maxTempDeadAccounts accounts so a pool-wide blip can't fan // capped at maxTempDeadAccounts accounts so a pool-wide blip can't fan
// a single request out across everything. // a single request out across everything.
// - 参数错 / request-level (anything else) → return immediately, no retry, no // - 参数错 / request-level (anything else) → return immediately, no retry, no
// account penalty (the account isn't at fault). // account penalty (the account isn't at fault).
// //
+2 -2
View File
@@ -526,9 +526,9 @@ onUnmounted(() => {
<div> <div>
<div class="flex items-center justify-between mb-1.5"> <div class="flex items-center justify-between mb-1.5">
<label class="block text-xs font-medium text-slate-500">提示词</label> <label class="block text-xs font-medium text-slate-500">提示词</label>
<span class="text-[11px] tabular-nums" :class="prompt.length >= 1500 ? 'text-rose-500' : 'text-slate-400'">{{ prompt.length }}/1500</span> <span class="text-[11px] tabular-nums text-slate-400">{{ prompt.length }}</span>
</div> </div>
<textarea v-model="prompt" rows="4" maxlength="1500" class="field resize-none disabled:opacity-60 disabled:cursor-not-allowed" <textarea v-model="prompt" rows="4" class="field resize-none disabled:opacity-60 disabled:cursor-not-allowed"
placeholder="描述想要的画面…如:黄昏时分,金色麦田里奔跑的金毛猎犬,电影感"></textarea> placeholder="描述想要的画面…如:黄昏时分,金色麦田里奔跑的金毛猎犬,电影感"></textarea>
</div> </div>