vidspotai-shared 1.0.101-dev.0 → 1.0.101
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/globals/aiModels/providers/google.d.ts.map +1 -1
- package/lib/globals/aiModels/providers/google.js +9 -6
- package/lib/globals/creditUnit.d.ts +33 -20
- package/lib/globals/creditUnit.d.ts.map +1 -1
- package/lib/globals/creditUnit.js +33 -20
- package/lib/services/aiGen/providers/alibaba/alibaba.d.ts +6 -0
- package/lib/services/aiGen/providers/alibaba/alibaba.d.ts.map +1 -1
- package/lib/services/aiGen/providers/alibaba/alibaba.js +103 -5
- package/lib/services/aiGen/providers/bytedance/bytedance.service.d.ts.map +1 -1
- package/lib/services/aiGen/providers/bytedance/bytedance.service.js +9 -1
- package/lib/services/aiGen/providers/google/google.service.d.ts.map +1 -1
- package/lib/services/aiGen/providers/google/google.service.js +142 -26
- package/lib/services/aiGen/providers/google/googleErrors.d.ts.map +1 -1
- package/lib/services/aiGen/providers/google/googleErrors.js +17 -7
- package/lib/services/translation/translation.service.d.ts.map +1 -1
- package/lib/services/translation/translation.service.js +26 -7
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google.d.ts","sourceRoot":"","sources":["../../../../src/globals/aiModels/providers/google.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAExC,eAAO,MAAM,aAAa,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,YAAY,CAAC,
|
|
1
|
+
{"version":3,"file":"google.d.ts","sourceRoot":"","sources":["../../../../src/globals/aiModels/providers/google.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAExC,eAAO,MAAM,aAAa,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,YAAY,CAAC,CA+ZpE,CAAC"}
|
|
@@ -109,12 +109,15 @@ exports.googleConfigs = {
|
|
|
109
109
|
// throws "seed parameter is not supported in Gemini API" client-side.
|
|
110
110
|
// Kept for reference; restore only under a Vertex (vertexai:true) client.
|
|
111
111
|
// seed: {},
|
|
112
|
-
// NOTE (applies to all Veo 3.x):
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
112
|
+
// NOTE (applies to all Veo 3.x): the legal value is MODE-dependent on the
|
|
113
|
+
// Gemini API — "allow_all" for text-to-video and extension, "allow_adult"
|
|
114
|
+
// for image-to-video / interpolation / reference images. "dont_allow" is
|
|
115
|
+
// Veo-2 legacy and returns 400 "<value> for personGeneration is currently
|
|
116
|
+
// not supported". allowedValues is kept as the union so validateParams
|
|
117
|
+
// doesn't hard-throw on a planner-emitted value; google.service.ts picks
|
|
118
|
+
// the value legal for the request's mode and drops the rest.
|
|
119
|
+
// (Regional caveat: in EU/UK/CH/MENA, Veo 3.x allows "allow_adult" ONLY,
|
|
120
|
+
// for every mode — so an EU-billed key would reject allow_all outright.)
|
|
118
121
|
personGeneration: { allowedValues: ["allow_all", "allow_adult", "dont_allow"] },
|
|
119
122
|
promptOptimizer: {},
|
|
120
123
|
},
|
|
@@ -42,32 +42,45 @@ export declare const MIN_CREDITS_PER_PAID_OP = 10;
|
|
|
42
42
|
*/
|
|
43
43
|
export declare const MIN_CREDITS_PER_IMAGE_OP = 2;
|
|
44
44
|
/**
|
|
45
|
-
* Credits granted on SIGNUP
|
|
46
|
-
*
|
|
45
|
+
* Credits granted on SIGNUP — one full floored generation ($1.00 of provider
|
|
46
|
+
* cost), no card required. This is the top of the funnel: a new account can try
|
|
47
|
+
* the product once before being asked for anything.
|
|
47
48
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* consumed credits, and only 88 who ever spent past the free grant — a 0.59%
|
|
52
|
-
* conversion on roughly $15k of given-away provider cost. Combined with the
|
|
53
|
-
* refund-on-failure path, single accounts reached 274 jobs and 100 delivered
|
|
54
|
-
* videos on a 10-credit balance.
|
|
49
|
+
* Briefly set to 0 on 2026-07-22 (free generation moved behind the card-backed
|
|
50
|
+
* trial) and restored to 10 on 2026-07-24 — the acquisition cost of a
|
|
51
|
+
* card-walled first run was judged worse than the give-away.
|
|
55
52
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
53
|
+
* Read this before raising it, because the give-away is real and measured:
|
|
54
|
+
* 23,962 users, 15,021 of whom consumed credits, and only 88 who ever spent
|
|
55
|
+
* past the free grant — a 0.59% conversion on roughly $15k of provider cost.
|
|
56
|
+
* The grant is handed out with no card, no email verification, and no
|
|
57
|
+
* device/IP dedup, so it is farmable by making another Google account. The
|
|
58
|
+
* mitigations that make 10 tolerable are:
|
|
59
|
+
* - the refund-on-failure loop is closed (see failureBilling.ts) — that loop,
|
|
60
|
+
* not the grant itself, is how single accounts reached 274 jobs and 100
|
|
61
|
+
* delivered videos on a 10-credit balance;
|
|
62
|
+
* - the floor is exactly one generation, so a farmed account gets one video.
|
|
60
63
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
64
|
+
* Anything ABOVE 10 needs an anti-abuse gate first (verified email + App Check
|
|
65
|
+
* at minimum).
|
|
63
66
|
*/
|
|
64
|
-
export declare const SIGNUP_GRANT_CREDITS =
|
|
67
|
+
export declare const SIGNUP_GRANT_CREDITS = 10;
|
|
65
68
|
/**
|
|
66
69
|
* Credits granted when a card-backed free trial actually starts (the
|
|
67
|
-
* `isTrialInvoice` branch in payment.service.handleInvoiceSucceeded).
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
70
|
+
* `isTrialInvoice` branch in payment.service.handleInvoiceSucceeded).
|
|
71
|
+
*
|
|
72
|
+
* ADDITIVE since 2026-07-24: that branch applies `FieldValue.increment` to
|
|
73
|
+
* `credits.total` and leaves `consumed` alone, so starting the trial always
|
|
74
|
+
* moves available credits by exactly +10 on top of whatever is left of the
|
|
75
|
+
* signup grant. Signup + trial with nothing spent = 20 available.
|
|
76
|
+
*
|
|
77
|
+
* It used to overwrite (`total: 10, consumed: 0`), which was invisible while
|
|
78
|
+
* SIGNUP_GRANT_CREDITS was 0 but became a silent no-op for users who added a
|
|
79
|
+
* card without having spent their signup grant. Paid (non-trial) invoices still
|
|
80
|
+
* SET the balance to the plan's allowance — those are a purchase, not a bonus.
|
|
81
|
+
*
|
|
82
|
+
* Deliberately equal to MIN_CREDITS_PER_PAID_OP so the trial is exactly one
|
|
83
|
+
* full generation of any kind.
|
|
71
84
|
*/
|
|
72
85
|
export declare const TRIAL_GRANT_CREDITS = 10;
|
|
73
86
|
//# sourceMappingURL=creditUnit.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"creditUnit.d.ts","sourceRoot":"","sources":["../../src/globals/creditUnit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,iDAAiD;AACjD,eAAO,MAAM,cAAc,MAAM,CAAC;AAElC,0EAA0E;AAC1E,eAAO,MAAM,eAAe,QAAqB,CAAC;AAElD;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAE1C;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAE1C
|
|
1
|
+
{"version":3,"file":"creditUnit.d.ts","sourceRoot":"","sources":["../../src/globals/creditUnit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,iDAAiD;AACjD,eAAO,MAAM,cAAc,MAAM,CAAC;AAElC,0EAA0E;AAC1E,eAAO,MAAM,eAAe,QAAqB,CAAC;AAElD;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAE1C;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAE1C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,mBAAmB,KAA0B,CAAC"}
|
|
@@ -45,31 +45,44 @@ exports.MIN_CREDITS_PER_PAID_OP = 10;
|
|
|
45
45
|
*/
|
|
46
46
|
exports.MIN_CREDITS_PER_IMAGE_OP = 2;
|
|
47
47
|
/**
|
|
48
|
-
* Credits granted on SIGNUP
|
|
49
|
-
*
|
|
48
|
+
* Credits granted on SIGNUP — one full floored generation ($1.00 of provider
|
|
49
|
+
* cost), no card required. This is the top of the funnel: a new account can try
|
|
50
|
+
* the product once before being asked for anything.
|
|
50
51
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* consumed credits, and only 88 who ever spent past the free grant — a 0.59%
|
|
55
|
-
* conversion on roughly $15k of given-away provider cost. Combined with the
|
|
56
|
-
* refund-on-failure path, single accounts reached 274 jobs and 100 delivered
|
|
57
|
-
* videos on a 10-credit balance.
|
|
52
|
+
* Briefly set to 0 on 2026-07-22 (free generation moved behind the card-backed
|
|
53
|
+
* trial) and restored to 10 on 2026-07-24 — the acquisition cost of a
|
|
54
|
+
* card-walled first run was judged worse than the give-away.
|
|
58
55
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
56
|
+
* Read this before raising it, because the give-away is real and measured:
|
|
57
|
+
* 23,962 users, 15,021 of whom consumed credits, and only 88 who ever spent
|
|
58
|
+
* past the free grant — a 0.59% conversion on roughly $15k of provider cost.
|
|
59
|
+
* The grant is handed out with no card, no email verification, and no
|
|
60
|
+
* device/IP dedup, so it is farmable by making another Google account. The
|
|
61
|
+
* mitigations that make 10 tolerable are:
|
|
62
|
+
* - the refund-on-failure loop is closed (see failureBilling.ts) — that loop,
|
|
63
|
+
* not the grant itself, is how single accounts reached 274 jobs and 100
|
|
64
|
+
* delivered videos on a 10-credit balance;
|
|
65
|
+
* - the floor is exactly one generation, so a farmed account gets one video.
|
|
63
66
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
67
|
+
* Anything ABOVE 10 needs an anti-abuse gate first (verified email + App Check
|
|
68
|
+
* at minimum).
|
|
66
69
|
*/
|
|
67
|
-
exports.SIGNUP_GRANT_CREDITS =
|
|
70
|
+
exports.SIGNUP_GRANT_CREDITS = 10;
|
|
68
71
|
/**
|
|
69
72
|
* Credits granted when a card-backed free trial actually starts (the
|
|
70
|
-
* `isTrialInvoice` branch in payment.service.handleInvoiceSucceeded).
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
73
|
+
* `isTrialInvoice` branch in payment.service.handleInvoiceSucceeded).
|
|
74
|
+
*
|
|
75
|
+
* ADDITIVE since 2026-07-24: that branch applies `FieldValue.increment` to
|
|
76
|
+
* `credits.total` and leaves `consumed` alone, so starting the trial always
|
|
77
|
+
* moves available credits by exactly +10 on top of whatever is left of the
|
|
78
|
+
* signup grant. Signup + trial with nothing spent = 20 available.
|
|
79
|
+
*
|
|
80
|
+
* It used to overwrite (`total: 10, consumed: 0`), which was invisible while
|
|
81
|
+
* SIGNUP_GRANT_CREDITS was 0 but became a silent no-op for users who added a
|
|
82
|
+
* card without having spent their signup grant. Paid (non-trial) invoices still
|
|
83
|
+
* SET the balance to the plan's allowance — those are a purchase, not a bonus.
|
|
84
|
+
*
|
|
85
|
+
* Deliberately equal to MIN_CREDITS_PER_PAID_OP so the trial is exactly one
|
|
86
|
+
* full generation of any kind.
|
|
74
87
|
*/
|
|
75
88
|
exports.TRIAL_GRANT_CREDITS = exports.MIN_CREDITS_PER_PAID_OP;
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { BaseAiGenProviderService } from "../baseAiGenProvider.service";
|
|
2
2
|
import { CreditUsageParams, ImageGenerationParams, ImageGenerationResult, VideoGenerationParams, VideoGenerationResult, VideoStatusParams, VideoStatusResult } from "../types";
|
|
3
|
+
/**
|
|
4
|
+
* Map a canonical "W:H" aspect ratio to a DashScope pixel `size` string.
|
|
5
|
+
* Returns undefined for unparseable input so the caller simply omits `size`
|
|
6
|
+
* and lets DashScope apply its own default (better than sending a bad value).
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveDashScopeSize(modelId: string, aspectRatio: string): string | undefined;
|
|
3
9
|
export declare class AlibabaService extends BaseAiGenProviderService {
|
|
4
10
|
private readonly baseUrl;
|
|
5
11
|
private readonly timeout;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"alibaba.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/alibaba/alibaba.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"alibaba.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/alibaba/alibaba.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAoGlB;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,GAClB,MAAM,GAAG,SAAS,CAiBpB;AAED,qBAAa,cAAe,SAAQ,wBAAwB;IAE1D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;;IAiBjC;;;;OAIG;YACW,OAAO;IAwDf,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAiE3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,EACzB,QAAQ,GACT,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IA0EjD;;;;;;;;;;;;;;;;;;;;;OAqBG;IACG,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IA4EjC;;;OAGG;YACW,iBAAiB;IA4C/B;;;OAGG;YACW,kBAAkB;IA6DhC,aAAa,CAAC,EACZ,QAAQ,EACR,UAAU,EACV,WAAW,EACX,QAAQ,EACR,SAAiB,EACjB,SAAa,EACb,WAAW,GACZ,EAAE,iBAAiB,GAAG,MAAM;CAoB9B"}
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.AlibabaService = void 0;
|
|
7
|
+
exports.resolveDashScopeSize = resolveDashScopeSize;
|
|
7
8
|
const axios_1 = __importDefault(require("axios"));
|
|
8
9
|
const types_1 = require("../../../../globals/types");
|
|
9
10
|
const baseAiGenProvider_service_1 = require("../baseAiGenProvider.service");
|
|
@@ -59,6 +60,68 @@ function isSyncMultimodalImageModel(modelId) {
|
|
|
59
60
|
function isAsyncWanImageModel(modelId) {
|
|
60
61
|
return modelId.startsWith("wan2.7-image") || modelId.startsWith("wan2.6-image");
|
|
61
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* DashScope's `size` parameter is "WIDTH*HEIGHT" in PIXELS — it is NOT an
|
|
65
|
+
* aspect-ratio string. We used to assign `parameters.size = params.aspectRatio`
|
|
66
|
+
* directly, so every request with an aspect ratio shipped `size: "9:16"` and
|
|
67
|
+
* came back `400 InvalidParameter` (observed in prod on qwen-image-2.0-pro,
|
|
68
|
+
* 2026-07-26). The 400 body was also swallowed (see `request`), which is why it
|
|
69
|
+
* only ever surfaced as the opaque "Request failed with status code 400".
|
|
70
|
+
*
|
|
71
|
+
* Two documented preset families, distinguished by their pixel budget:
|
|
72
|
+
* - qwen-image-2.x → 2048×2048-class (square preset 2048*2048)
|
|
73
|
+
* - everything else on this endpoint (max / plus / edit / wan2.7)
|
|
74
|
+
* → 1328×1328-class (square preset 1328*1328)
|
|
75
|
+
* Values below are the documented allowed presets, so they are guaranteed-valid
|
|
76
|
+
* rather than inferred. Ratios with no published preset (2:3, 3:2) are computed
|
|
77
|
+
* from the family's square-preset AREA and rounded to a multiple of 16, which
|
|
78
|
+
* keeps them inside the documented total-pixel range.
|
|
79
|
+
*/
|
|
80
|
+
const DASHSCOPE_SIZE_PRESETS = {
|
|
81
|
+
// qwen-image-2.x series
|
|
82
|
+
large: {
|
|
83
|
+
"1:1": "2048*2048",
|
|
84
|
+
"16:9": "2688*1536",
|
|
85
|
+
"9:16": "1536*2688",
|
|
86
|
+
"4:3": "2368*1728",
|
|
87
|
+
"3:4": "1728*2368",
|
|
88
|
+
},
|
|
89
|
+
// qwen-image-max / -plus / -edit, wan2.7-image
|
|
90
|
+
standard: {
|
|
91
|
+
"1:1": "1328*1328",
|
|
92
|
+
"16:9": "1664*928",
|
|
93
|
+
"9:16": "928*1664",
|
|
94
|
+
"4:3": "1472*1104",
|
|
95
|
+
"3:4": "1104*1472",
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
/** Round to the nearest multiple of 16 (DashScope dimensions are 16-aligned). */
|
|
99
|
+
function round16(n) {
|
|
100
|
+
return Math.max(16, Math.round(n / 16) * 16);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Map a canonical "W:H" aspect ratio to a DashScope pixel `size` string.
|
|
104
|
+
* Returns undefined for unparseable input so the caller simply omits `size`
|
|
105
|
+
* and lets DashScope apply its own default (better than sending a bad value).
|
|
106
|
+
*/
|
|
107
|
+
function resolveDashScopeSize(modelId, aspectRatio) {
|
|
108
|
+
const family = modelId.startsWith("qwen-image-2") ? "large" : "standard";
|
|
109
|
+
const preset = DASHSCOPE_SIZE_PRESETS[family][aspectRatio];
|
|
110
|
+
if (preset)
|
|
111
|
+
return preset;
|
|
112
|
+
// No published preset for this ratio — derive one at the family's area.
|
|
113
|
+
const [wStr, hStr] = aspectRatio.split(":");
|
|
114
|
+
const w = Number(wStr);
|
|
115
|
+
const h = Number(hStr);
|
|
116
|
+
if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
const squarePreset = DASHSCOPE_SIZE_PRESETS[family]["1:1"];
|
|
120
|
+
const side = Number(squarePreset.split("*")[0]);
|
|
121
|
+
const area = side * side;
|
|
122
|
+
const ratio = w / h;
|
|
123
|
+
return `${round16(Math.sqrt(area * ratio))}*${round16(Math.sqrt(area / ratio))}`;
|
|
124
|
+
}
|
|
62
125
|
class AlibabaService extends baseAiGenProvider_service_1.BaseAiGenProviderService {
|
|
63
126
|
constructor() {
|
|
64
127
|
super();
|
|
@@ -93,8 +156,32 @@ class AlibabaService extends baseAiGenProvider_service_1.BaseAiGenProviderServic
|
|
|
93
156
|
timeout: this.timeout,
|
|
94
157
|
data: method === "POST" ? body : undefined,
|
|
95
158
|
};
|
|
96
|
-
|
|
97
|
-
|
|
159
|
+
try {
|
|
160
|
+
const res = await axios_1.default.request(config);
|
|
161
|
+
return res.data;
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
// DashScope puts the real reason (code + message, e.g.
|
|
165
|
+
// "InvalidParameter: size is invalid") in the RESPONSE BODY. Axios's own
|
|
166
|
+
// message is only "Request failed with status code 400", so without this
|
|
167
|
+
// the actual cause was lost — a prod 400 on qwen-image-2.0-pro was
|
|
168
|
+
// undiagnosable until the body was surfaced (2026-07-26). Re-throw with
|
|
169
|
+
// the provider's code/message appended so it reaches Loki/Slack and the
|
|
170
|
+
// stored job errorMessage.
|
|
171
|
+
const axiosErr = err;
|
|
172
|
+
const data = axiosErr.response?.data;
|
|
173
|
+
if (data && (data.code || data.message)) {
|
|
174
|
+
logger_1.logger.error("Alibaba DashScope request failed", {
|
|
175
|
+
url,
|
|
176
|
+
status: axiosErr.response?.status,
|
|
177
|
+
code: data.code,
|
|
178
|
+
message: data.message,
|
|
179
|
+
requestId: data.request_id,
|
|
180
|
+
});
|
|
181
|
+
throw new Error(`DashScope ${axiosErr.response?.status ?? ""} ${data.code ?? ""}: ${data.message ?? axiosErr.message ?? "request failed"}`.trim());
|
|
182
|
+
}
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
98
185
|
}
|
|
99
186
|
async generateVideo(params) {
|
|
100
187
|
(0, helpers_2.validateParams)(params);
|
|
@@ -310,8 +397,13 @@ class AlibabaService extends baseAiGenProvider_service_1.BaseAiGenProviderServic
|
|
|
310
397
|
parameters.prompt_extend = params.promptOptimizer;
|
|
311
398
|
if (params.seed !== undefined)
|
|
312
399
|
parameters.seed = params.seed;
|
|
313
|
-
|
|
314
|
-
|
|
400
|
+
// `size` is WIDTH*HEIGHT pixels, never a ratio string — see
|
|
401
|
+
// resolveDashScopeSize. An explicit imageSize (already in pixel form) wins.
|
|
402
|
+
if (params.aspectRatio) {
|
|
403
|
+
const size = resolveDashScopeSize(modelId, params.aspectRatio);
|
|
404
|
+
if (size)
|
|
405
|
+
parameters.size = size;
|
|
406
|
+
}
|
|
315
407
|
if (params.imageSize)
|
|
316
408
|
parameters.size = params.imageSize;
|
|
317
409
|
const result = await this.request({ model: modelId, input, parameters }, "POST", ENDPOINT_MULTIMODAL_SYNC, false);
|
|
@@ -332,7 +424,13 @@ class AlibabaService extends baseAiGenProvider_service_1.BaseAiGenProviderServic
|
|
|
332
424
|
input.negative_prompt = params.negativePrompt;
|
|
333
425
|
const parameters = {
|
|
334
426
|
n: params.numImages ?? 1,
|
|
335
|
-
|
|
427
|
+
// Same pixel-vs-ratio rule as the sync path — see resolveDashScopeSize.
|
|
428
|
+
...(params.aspectRatio
|
|
429
|
+
? (() => {
|
|
430
|
+
const size = resolveDashScopeSize(modelId, params.aspectRatio);
|
|
431
|
+
return size ? { size } : {};
|
|
432
|
+
})()
|
|
433
|
+
: {}),
|
|
336
434
|
...(params.imageSize ? { size: params.imageSize } : {}),
|
|
337
435
|
...(params.seed !== undefined ? { seed: params.seed } : {}),
|
|
338
436
|
...(params.watermark !== undefined ? { watermark: params.watermark } : {}),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bytedance.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/bytedance/bytedance.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAUlB,qBAAa,gBAAiB,SAAQ,wBAAwB;IAC5D,OAAO,CAAC,QAAQ,CAAC,OAAO,CACsD;IAI9E,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,WAAW;IAIb,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"bytedance.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/bytedance/bytedance.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAUlB,qBAAa,gBAAiB,SAAQ,wBAAwB;IAC5D,OAAO,CAAC,QAAQ,CAAC,OAAO,CACsD;IAI9E,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,WAAW;IAIb,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAqM3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IA4HjD,aAAa,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM;CAyEjD"}
|
|
@@ -38,7 +38,15 @@ class ByteDanceService extends baseAiGenProvider_service_1.BaseAiGenProviderServ
|
|
|
38
38
|
const modelId = modelConfig.modelId;
|
|
39
39
|
const sanitizedPrompt = this.sanitizePrompt(params.prompt ?? "");
|
|
40
40
|
if (!sanitizedPrompt) {
|
|
41
|
-
|
|
41
|
+
// PROMPT_INVALID (not an uncoded throw): sanitizePrompt strips everything
|
|
42
|
+
// outside ASCII+CJK, so an empty result means the prompt was written in a
|
|
43
|
+
// script Seedance cannot accept (Cyrillic, Arabic, Devanagari, Thai, an
|
|
44
|
+
// all-emoji prompt, …). That is deterministic user input — it can never
|
|
45
|
+
// succeed on retry. Without a code, `isNonPagingCode(undefined)` is false
|
|
46
|
+
// and the scene-failure handler logged it at `error`, which pages Slack
|
|
47
|
+
// (see errorPaging.ts). PROMPT_INVALID is in NON_PAGING_ERROR_CODES, so
|
|
48
|
+
// this now stays a warn while still failing the scene cleanly.
|
|
49
|
+
throw new errors_1.UserFacingError("Your prompt could not be processed. Please use English or Chinese text and try again.", errors_1.USER_FACING_ERROR_CODES.PROMPT_INVALID);
|
|
42
50
|
}
|
|
43
51
|
const isV2 = this.isSeedance2(params.modelKey);
|
|
44
52
|
const content = [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/google/google.service.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAkElB,qBAAa,aAAc,SAAQ,wBAAwB;IAKzD,OAAO,CAAC,EAAE,CAAc;IACxB,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAK;;IAQ/C;;;;;;;;;OASG;IACG,YAAY,CAChB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,oBAAoB,CAAC;IAoChC;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IAoCvB;;;;OAIG;YACW,kBAAkB;IAsC1B,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"google.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/google/google.service.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAkElB,qBAAa,aAAc,SAAQ,wBAAwB;IAKzD,OAAO,CAAC,EAAE,CAAc;IACxB,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAK;;IAQ/C;;;;;;;;;OASG;IACG,YAAY,CAChB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,oBAAoB,CAAC;IAoChC;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IAoCvB;;;;OAIG;YACW,kBAAkB;IAsC1B,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IA2T3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IA8J3C,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;YAiBnB,cAAc;IAyJ5B;;;;;;OAMG;IACH;;;;OAIG;IACG,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAIjC,aAAa,CAAC,EAAE,QAAQ,EAAE,QAAY,EAAE,UAAmB,EAAE,SAAiB,EAAE,SAAa,EAAE,SAAS,EAAE,WAAW,EAAE,EAAE,iBAAiB,GAAG,MAAM;CA8BpJ"}
|
|
@@ -197,20 +197,54 @@ class GoogleService extends baseAiGenProvider_service_1.BaseAiGenProviderService
|
|
|
197
197
|
const modelConfig = aiModels_1.aiModelConfigs[params.modelKey];
|
|
198
198
|
const modelId = modelConfig.modelId;
|
|
199
199
|
const isVeo3_1 = VEO_3_1_MODELS.has(params.modelKey);
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
200
|
+
// ── Veo generation mode ──────────────────────────────────────────────
|
|
201
|
+
// Several Veo constraints are MODE-dependent (not just model-dependent),
|
|
202
|
+
// so resolve the mode once and drive every guard below off it. Precedence
|
|
203
|
+
// matches the request builder further down: video extension wins over an
|
|
204
|
+
// input image (they are mutually exclusive in the Veo API).
|
|
205
|
+
const isExtension = !!params.inputVideoUrl;
|
|
206
|
+
const refCount = params.referenceImageUrls?.length ?? 0;
|
|
207
|
+
const isImageDriven = !isExtension &&
|
|
208
|
+
(!!params.inputImageUrl ||
|
|
209
|
+
!!params.lastFrameImageUrl ||
|
|
210
|
+
(isVeo3_1 && refCount > 0));
|
|
211
|
+
// Gemini Veo cross-constraint: extension, first+last-frame interpolation
|
|
212
|
+
// AND reference images ALL require durationSeconds=8 (per the Veo API
|
|
213
|
+
// parameter table: `durationSeconds` "Must be '8' when using extension,
|
|
214
|
+
// reference images or with 1080p and 4k resolutions"). Sending any other
|
|
215
|
+
// duration returns INVALID_ARGUMENT 400 with no hint about which param
|
|
216
|
+
// caused it. Surface a typed UserFacingError so the user/frontend can
|
|
217
|
+
// correct the input instead of burning a provider call + opaque rejection.
|
|
218
|
+
const needsDuration8 = isExtension ||
|
|
219
|
+
!!params.lastFrameImageUrl ||
|
|
220
|
+
(isVeo3_1 && refCount > 0);
|
|
208
221
|
if (needsDuration8 && params.duration !== undefined && params.duration !== 8) {
|
|
209
|
-
const constraint =
|
|
210
|
-
? "
|
|
211
|
-
:
|
|
222
|
+
const constraint = isExtension
|
|
223
|
+
? "video extension"
|
|
224
|
+
: params.lastFrameImageUrl
|
|
225
|
+
? "first-frame + last-frame interpolation"
|
|
226
|
+
: "reference images";
|
|
227
|
+
const remedy = isExtension
|
|
228
|
+
? "extend a shorter clip"
|
|
229
|
+
: `remove the ${params.lastFrameImageUrl ? "last-frame image" : "reference images"}`;
|
|
212
230
|
throw new errors_1.UserFacingError(`Google Veo requires an 8-second duration when using ${constraint}. ` +
|
|
213
|
-
`Please select 8s or
|
|
231
|
+
`Please select 8s or ${remedy}.`, errors_1.USER_FACING_ERROR_CODES.CAPABILITY_MISMATCH);
|
|
232
|
+
}
|
|
233
|
+
// Extension output is capped at 720p ("`720p` only for extension" in the
|
|
234
|
+
// same table). A 1080p/4k extend is a deterministic 400, so reject it here
|
|
235
|
+
// rather than paying for the round-trip. NOT coerced to 720p on purpose:
|
|
236
|
+
// resolution drives the credit price (cost.perResolution), so silently
|
|
237
|
+
// downgrading the output would bill the user for a tier we didn't deliver.
|
|
238
|
+
if (isExtension && params.resolution && params.resolution !== "720p") {
|
|
239
|
+
throw new errors_1.UserFacingError(`Google Veo only supports 720p when extending a video (requested ${params.resolution}). ` +
|
|
240
|
+
`Please select 720p to extend this clip.`, errors_1.USER_FACING_ERROR_CODES.CAPABILITY_MISMATCH);
|
|
241
|
+
}
|
|
242
|
+
// `lastFrame` is the END of an interpolation and is only meaningful with a
|
|
243
|
+
// START frame — the API spec states it "Must be used in combination with
|
|
244
|
+
// the `image` parameter." Sending it alone is a deterministic 400.
|
|
245
|
+
if (params.lastFrameImageUrl && !params.inputImageUrl) {
|
|
246
|
+
throw new errors_1.UserFacingError("Google Veo needs a starting image as well as a last-frame image to interpolate between them. " +
|
|
247
|
+
"Please add a first-frame image or remove the last-frame image.", errors_1.USER_FACING_ERROR_CODES.CAPABILITY_MISMATCH);
|
|
214
248
|
}
|
|
215
249
|
const request = {
|
|
216
250
|
model: modelId,
|
|
@@ -228,18 +262,25 @@ class GoogleService extends baseAiGenProvider_service_1.BaseAiGenProviderService
|
|
|
228
262
|
// carried a seed. Like `generateAudio`, it is a Vertex-AI-only field.
|
|
229
263
|
// Restore this line if/when this client switches to Vertex (vertexai:true).
|
|
230
264
|
// ...(params.seed !== undefined ? { seed: params.seed } : {}),
|
|
231
|
-
// personGeneration on the Gemini API Veo backend
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
|
|
265
|
+
// personGeneration on the Gemini API Veo backend is MODE-dependent, not
|
|
266
|
+
// just model-dependent. Per the Veo API parameter table for Veo 3.1 /
|
|
267
|
+
// 3.1 Fast / 3.1 Lite:
|
|
268
|
+
// • Text-to-video & extension → "allow_all" ONLY
|
|
269
|
+
// • Image-to-video, interpolation, reference images → "allow_adult" ONLY
|
|
270
|
+
// The previous comment here claimed allow_all was the only valid value
|
|
271
|
+
// for all of Veo 3.x; that is true only for the text/extension modes.
|
|
272
|
+
// Forwarding allow_all on an image-driven request is a deterministic
|
|
273
|
+
// 400. "dont_allow" is Veo-2-only legacy and is never valid on 3.x.
|
|
274
|
+
//
|
|
275
|
+
// So: forward only the value that is legal for THIS mode, and drop
|
|
276
|
+
// anything else rather than hard-failing (the API default for each mode
|
|
277
|
+
// is that mode's single legal value, so omitting is always safe — it is
|
|
278
|
+
// what the overwhelming majority of our traffic already does).
|
|
279
|
+
...(params.personGeneration === "allow_all" ||
|
|
280
|
+
params.personGeneration === "allow_adult"
|
|
281
|
+
? isImageDriven
|
|
282
|
+
? { personGeneration: "allow_adult" }
|
|
283
|
+
: { personGeneration: "allow_all" }
|
|
243
284
|
: {}),
|
|
244
285
|
// negativePrompt support is MODEL-SPECIFIC on the Gemini API: veo-3.1 and
|
|
245
286
|
// veo-3.1-fast accept it, veo-3.1-lite returns 400 "negativePrompt isn't
|
|
@@ -323,8 +364,36 @@ class GoogleService extends baseAiGenProvider_service_1.BaseAiGenProviderService
|
|
|
323
364
|
const keyFailure = (0, googleErrors_1.classifyGoogleKeyHealth)(err);
|
|
324
365
|
// Not a fail-over-able failure (e.g. content rejection, bad params) →
|
|
325
366
|
// don't burn other accounts on it; surface as-is.
|
|
326
|
-
if (!keyFailure)
|
|
367
|
+
if (!keyFailure) {
|
|
368
|
+
// Log WHAT WE SENT before rethrowing. Veo answers an unsupported
|
|
369
|
+
// parameter combination with a generic 400 ("Unsupported video
|
|
370
|
+
// generation request", "Your use case is currently not supported")
|
|
371
|
+
// that names neither the offending field nor its value — so without
|
|
372
|
+
// this line the Slack alert and the job doc are both dead ends, and
|
|
373
|
+
// the combo can only be guessed at (prod 2026-07-26). Shape only: no
|
|
374
|
+
// prompt text, no image bytes, no signed URLs.
|
|
375
|
+
logger_1.logger.warn("Google Veo: request rejected — submitted shape", {
|
|
376
|
+
modelKey: params.modelKey,
|
|
377
|
+
modelId,
|
|
378
|
+
aspectRatio: params.aspectRatio,
|
|
379
|
+
resolution: params.resolution,
|
|
380
|
+
durationSeconds: params.duration,
|
|
381
|
+
mode: isExtension
|
|
382
|
+
? "extension"
|
|
383
|
+
: isImageDriven
|
|
384
|
+
? "image-driven"
|
|
385
|
+
: "text-to-video",
|
|
386
|
+
hasInputImage: !!params.inputImageUrl,
|
|
387
|
+
hasLastFrame: !!params.lastFrameImageUrl,
|
|
388
|
+
referenceImageCount: refCount,
|
|
389
|
+
personGeneration: params.personGeneration ?? null,
|
|
390
|
+
hasNegativePrompt: !!params.negativePrompt,
|
|
391
|
+
enhancePrompt: params.promptOptimizer ?? null,
|
|
392
|
+
promptChars: params.prompt?.length ?? 0,
|
|
393
|
+
error: err instanceof Error ? err.message.slice(0, 500) : String(err),
|
|
394
|
+
});
|
|
327
395
|
throw err;
|
|
396
|
+
}
|
|
328
397
|
// Observability: log the RAW provider error once so we can see whether
|
|
329
398
|
// this was an RPM vs RPD limit and on which key (AI Studio's dashboard
|
|
330
399
|
// lags real-time, so this is our authoritative signal — Ammar 2026-06-20).
|
|
@@ -543,6 +612,11 @@ class GoogleService extends baseAiGenProvider_service_1.BaseAiGenProviderService
|
|
|
543
612
|
const isImagen = modelId.startsWith("imagen-");
|
|
544
613
|
const isNanoBanana = modelId.startsWith("gemini-") && modelId.includes("image");
|
|
545
614
|
const allBytes = [];
|
|
615
|
+
// Safety/RAI block reasons harvested from the response when it comes back
|
|
616
|
+
// WITHOUT image bytes. Google does not throw in this case — it returns a
|
|
617
|
+
// 200 with the images filtered out — so the reason is only available here.
|
|
618
|
+
// Mirrors the Veo video path (raiMediaFilteredReasons, above).
|
|
619
|
+
const blockReasons = [];
|
|
546
620
|
if (isImagen) {
|
|
547
621
|
const resp = await this.ai.models.generateImages({
|
|
548
622
|
model: modelId,
|
|
@@ -559,13 +633,25 @@ class GoogleService extends baseAiGenProvider_service_1.BaseAiGenProviderService
|
|
|
559
633
|
});
|
|
560
634
|
for (const gen of resp.generatedImages ?? []) {
|
|
561
635
|
const b64 = gen.image?.imageBytes;
|
|
562
|
-
if (!b64)
|
|
636
|
+
if (!b64) {
|
|
637
|
+
// An entry with no bytes carries the RAI filter reason instead.
|
|
638
|
+
const reason = gen.raiFilteredReason;
|
|
639
|
+
if (reason)
|
|
640
|
+
blockReasons.push(reason);
|
|
563
641
|
continue;
|
|
642
|
+
}
|
|
564
643
|
allBytes.push({
|
|
565
644
|
bytes: Buffer.from(b64, "base64"),
|
|
566
645
|
mime: gen.image?.mimeType ?? "image/png",
|
|
567
646
|
});
|
|
568
647
|
}
|
|
648
|
+
// Prompt-level rejection: Imagen filters the whole request before
|
|
649
|
+
// producing any generatedImages entry, reporting it here instead.
|
|
650
|
+
const promptReason = resp
|
|
651
|
+
.positivePromptSafetyAttributes;
|
|
652
|
+
if (!allBytes.length && promptReason && !blockReasons.length) {
|
|
653
|
+
blockReasons.push("Prompt blocked by Google's safety filters");
|
|
654
|
+
}
|
|
569
655
|
}
|
|
570
656
|
else if (isNanoBanana) {
|
|
571
657
|
// Nano Banana (Gemini image-gen via generateContent) has no structured
|
|
@@ -607,11 +693,41 @@ class GoogleService extends baseAiGenProvider_service_1.BaseAiGenProviderService
|
|
|
607
693
|
}
|
|
608
694
|
}
|
|
609
695
|
}
|
|
696
|
+
// Gemini reports a safety stop two ways, neither of which throws:
|
|
697
|
+
// promptFeedback.blockReason (whole prompt rejected → candidates empty)
|
|
698
|
+
// and candidate.finishReason (generation halted: IMAGE_SAFETY,
|
|
699
|
+
// PROHIBITED_CONTENT, RECITATION, SPII).
|
|
700
|
+
const promptBlock = resp
|
|
701
|
+
.promptFeedback?.blockReason;
|
|
702
|
+
if (promptBlock)
|
|
703
|
+
blockReasons.push(promptBlock);
|
|
704
|
+
for (const cand of candidates) {
|
|
705
|
+
const finish = cand.finishReason;
|
|
706
|
+
if (finish && finish !== "STOP" && finish !== "MAX_TOKENS") {
|
|
707
|
+
blockReasons.push(finish);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
610
710
|
}
|
|
611
711
|
else {
|
|
612
712
|
throw new Error(`Unsupported Google image modelId: ${modelId}`);
|
|
613
713
|
}
|
|
614
714
|
if (!allBytes.length) {
|
|
715
|
+
// A 200 with zero images is almost always a safety/RAI block, not a bug.
|
|
716
|
+
// Previously this threw a bare Error whose message wasn't JSON, so
|
|
717
|
+
// classifyGoogleApiError (which JSON.parses the message) returned null and
|
|
718
|
+
// the image queue logged it at `error` — paging Slack for what is really a
|
|
719
|
+
// user-content outcome. Emit a typed CONTENT_POLICY_VIOLATION when we have
|
|
720
|
+
// a block reason so it's logged at warn, and keep the reason for triage.
|
|
721
|
+
if (blockReasons.length) {
|
|
722
|
+
logger_1.logger.warn("Google image generation blocked by safety filters", {
|
|
723
|
+
modelKey: params.modelKey,
|
|
724
|
+
modelId,
|
|
725
|
+
reasons: blockReasons,
|
|
726
|
+
});
|
|
727
|
+
throw new errors_1.UserFacingError("Your prompt was flagged by Google's safety filters. Please rephrase and try again.", errors_1.USER_FACING_ERROR_CODES.CONTENT_POLICY_VIOLATION);
|
|
728
|
+
}
|
|
729
|
+
// No block reason at all — genuinely unexpected; keep it loud so a real
|
|
730
|
+
// platform/SDK regression isn't muted by the branch above.
|
|
615
731
|
throw new Error("Google image generation returned no image bytes");
|
|
616
732
|
}
|
|
617
733
|
const bucket = (0, firebase_1.getBucket)();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"googleErrors.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/google/googleErrors.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EAGhB,MAAM,0BAA0B,CAAC;AAElC;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,GAAG,GAAG,eAAe,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"googleErrors.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/google/googleErrors.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EAGhB,MAAM,0BAA0B,CAAC;AAElC;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,GAAG,GAAG,eAAe,GAAG,IAAI,CA8JvE;AAED;;;+EAG+E;AAC/E,MAAM,WAAW,sBAAsB;IACrC,4DAA4D;IAC5D,IAAI,EAAE,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC;IACnC,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;IACf,mFAAmF;IACnF,UAAU,EAAE,MAAM,CAAC;CACpB;AAcD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,GAAG,GAAG,sBAAsB,GAAG,IAAI,CAuE/E"}
|
|
@@ -49,18 +49,28 @@ function classifyGoogleApiError(err) {
|
|
|
49
49
|
if (httpCode === 14 || /high demand/i.test(msg)) {
|
|
50
50
|
return new errors_1.UserFacingError((0, errors_1.friendlyMessageForCode)(errors_1.USER_FACING_ERROR_CODES.VIDEO_PROVIDER_HIGH_DEMAND), errors_1.USER_FACING_ERROR_CODES.VIDEO_PROVIDER_HIGH_DEMAND);
|
|
51
51
|
}
|
|
52
|
-
// INVALID_ARGUMENT 400 —
|
|
53
|
-
//
|
|
54
|
-
//
|
|
52
|
+
// INVALID_ARGUMENT 400 — Veo's "this request shape isn't supported for this
|
|
53
|
+
// model variant" family. Two known phrasings, both meaning the same thing:
|
|
54
|
+
// - "Your use case is currently not supported" (older)
|
|
55
|
+
// - "Unsupported video generation request. Please check the documentation
|
|
56
|
+
// for supported usage: …/gemini-api/docs/video" (seen in prod on
|
|
57
|
+
// veo-3.1-fast, 2026-07-26 — recurring across many users)
|
|
55
58
|
// The pre-call guards above (duration=8 for lastFrame/refs) should
|
|
56
59
|
// prevent the known cases; if we still hit this it's a NEW combo we
|
|
57
60
|
// haven't profiled — surface as CAPABILITY_MISMATCH so the user gets a
|
|
58
61
|
// useful message, AND keep the raw provider text in the error so the
|
|
59
|
-
// next entry in PROD_FIX_LOG can identify which combo broke.
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
+
// next entry in PROD_FIX_LOG can identify which combo broke.
|
|
63
|
+
//
|
|
64
|
+
// Why this matters beyond the message: an unclassified 400 is not a
|
|
65
|
+
// UserFacingError, so the scene loop treats it as a candidate for the
|
|
66
|
+
// transient-retry path and logs it at `error` (which is what pages Slack —
|
|
67
|
+
// see errorPaging.ts). A deterministic 400 can never succeed on retry, so
|
|
68
|
+
// classifying it here both stops the wasted re-submits and moves it to
|
|
69
|
+
// `warn`, since CAPABILITY_MISMATCH is in NON_PAGING_ERROR_CODES.
|
|
70
|
+
// Generic 400s (other INVALID_ARGUMENT variants) still surface as `error`
|
|
71
|
+
// so a real platform bug isn't muted.
|
|
62
72
|
if ((status === "INVALID_ARGUMENT" || httpCode === 400) &&
|
|
63
|
-
/use case is currently not supported/i.test(msg)) {
|
|
73
|
+
/use case is currently not supported|unsupported video generation request/i.test(msg)) {
|
|
64
74
|
return new errors_1.UserFacingError(msg, errors_1.USER_FACING_ERROR_CODES.CAPABILITY_MISMATCH);
|
|
65
75
|
}
|
|
66
76
|
// Imagen + Nano-Banana surface Responsible-AI filter rejections as
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translation.service.d.ts","sourceRoot":"","sources":["../../../src/services/translation/translation.service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"translation.service.d.ts","sourceRoot":"","sources":["../../../src/services/translation/translation.service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAqGvD;;;;GAIG;AACH,iBAAe,SAAS,CACtB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,cAAc,EACpB,EAAE,GAAE,cAAkC,EACtC,OAAO,GAAE,IAAI,GAAG,IAAW,GAC1B,OAAO,CAAC,MAAM,CAAC,CAsCjB;AAiCD;;;;;;;;;;;;;;GAcG;AACH,iBAAe,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA8ChE;AAOD,eAAO,MAAM,kBAAkB;;;CAG9B,CAAC;AAGF,qDAAqD;AACrD,eAAO,MAAM,qBAAqB,kBAAY,CAAC;AAC/C,0DAA0D;AAC1D,eAAO,MAAM,UAAU,uBAAiB,CAAC"}
|
|
@@ -74,6 +74,13 @@ const PRIMARY_BASE = "https://us-central1-translation-service-3931b.cloudfunctio
|
|
|
74
74
|
// 24s user-facing stall was itself part of the problem).
|
|
75
75
|
const PRIMARY_TIMEOUT_MS = 6000;
|
|
76
76
|
const PRIMARY_MAX_ATTEMPTS = 2;
|
|
77
|
+
// The LLM fallback had NO timeout, so when the primary CF was also failing the
|
|
78
|
+
// two layers compounded into an unbounded stall — a Vietnamese request was
|
|
79
|
+
// measured at 48s in prod (2026-07-26): ~12s burning both primary attempts,
|
|
80
|
+
// then ~36s waiting on a slow generateText that nothing was bounding. Cap the
|
|
81
|
+
// fallback so the worst case is primary (~12.2s) + fallback (15s) ≈ 27s and the
|
|
82
|
+
// caller gets a definite answer instead of holding the request open.
|
|
83
|
+
const FALLBACK_TIMEOUT_MS = 15000;
|
|
77
84
|
// gpt-4o-mini: cheap (~$0.15/1M in, $0.60/1M out → fractions of a cent per
|
|
78
85
|
// prompt), reliable, and independent of the Google video quota. Swap here if a
|
|
79
86
|
// cheaper effective model becomes available.
|
|
@@ -110,13 +117,25 @@ async function callLlmTranslate(text, from, to) {
|
|
|
110
117
|
`Preserve meaning, tone, names, numbers, hashtags, emojis and line breaks. ` +
|
|
111
118
|
`Output ONLY the translated text — no quotes, no language labels, no commentary. ` +
|
|
112
119
|
`If the text is already in ${target}, return it unchanged.`;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
+
// Bounded: generateText itself takes no timeout, so race it. The loser is
|
|
121
|
+
// abandoned rather than cancelled (there is no AbortSignal on this path), but
|
|
122
|
+
// the caller is unblocked — which is the point.
|
|
123
|
+
let timer;
|
|
124
|
+
const { text: out } = await Promise.race([
|
|
125
|
+
service.generateText({
|
|
126
|
+
input: [
|
|
127
|
+
{ role: "system", content: system },
|
|
128
|
+
{ role: "user", content: text },
|
|
129
|
+
],
|
|
130
|
+
modelKey: FALLBACK_MODEL,
|
|
131
|
+
options: { temperature: 0 },
|
|
132
|
+
}),
|
|
133
|
+
new Promise((_, reject) => {
|
|
134
|
+
timer = setTimeout(() => reject(new Error(`LLM fallback timed out after ${FALLBACK_TIMEOUT_MS}ms`)), FALLBACK_TIMEOUT_MS);
|
|
135
|
+
}),
|
|
136
|
+
]).finally(() => {
|
|
137
|
+
if (timer)
|
|
138
|
+
clearTimeout(timer);
|
|
120
139
|
});
|
|
121
140
|
const trimmed = (out ?? "").trim();
|
|
122
141
|
if (!trimmed || trimmed === "No response") {
|