mirror of
https://git.mirrors.martin98.com/https://github.com/jina-ai/reader.git
synced 2025-08-19 19:09:12 +08:00
saas: new tier policy
This commit is contained in:
parent
33ca16405e
commit
5a81177c56
@ -240,6 +240,7 @@ export class CrawlerHost extends RPCHost {
|
|||||||
const uid = await auth.solveUID();
|
const uid = await auth.solveUID();
|
||||||
let chargeAmount = 0;
|
let chargeAmount = 0;
|
||||||
const crawlerOptions = ctx.method === 'GET' ? crawlerOptionsHeaderOnly : crawlerOptionsParamsAllowed;
|
const crawlerOptions = ctx.method === 'GET' ? crawlerOptionsHeaderOnly : crawlerOptionsParamsAllowed;
|
||||||
|
const tierPolicy = await this.saasAssertTierPolicy(crawlerOptions, auth);
|
||||||
|
|
||||||
// Use koa ctx.URL, a standard URL object to avoid node.js framework prop naming confusion
|
// Use koa ctx.URL, a standard URL object to avoid node.js framework prop naming confusion
|
||||||
const targetUrl = await this.getTargetUrl(tryDecodeURIComponent(`${ctx.URL.pathname}${ctx.URL.search}`), crawlerOptions);
|
const targetUrl = await this.getTargetUrl(tryDecodeURIComponent(`${ctx.URL.pathname}${ctx.URL.search}`), crawlerOptions);
|
||||||
@ -298,15 +299,13 @@ export class CrawlerHost extends RPCHost {
|
|||||||
if (crawlerOptions.tokenBudget && chargeAmount > crawlerOptions.tokenBudget) {
|
if (crawlerOptions.tokenBudget && chargeAmount > crawlerOptions.tokenBudget) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (chargeAmount) {
|
apiRoll.chargeAmount = chargeAmount;
|
||||||
apiRoll._ref?.set({
|
|
||||||
chargeAmount,
|
|
||||||
}, { merge: true }).catch((err) => this.logger.warn(`Failed to log charge amount in apiRoll`, { err }));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!uid) {
|
if (!uid) {
|
||||||
|
// Enforce no proxy is allocated for anonymous users due to abuse.
|
||||||
|
crawlerOptions.proxy = 'none';
|
||||||
const blockade = (await DomainBlockade.fromFirestoreQuery(
|
const blockade = (await DomainBlockade.fromFirestoreQuery(
|
||||||
DomainBlockade.COLLECTION
|
DomainBlockade.COLLECTION
|
||||||
.where('domain', '==', targetUrl.hostname.toLowerCase())
|
.where('domain', '==', targetUrl.hostname.toLowerCase())
|
||||||
@ -338,10 +337,7 @@ export class CrawlerHost extends RPCHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const formatted = await this.formatSnapshot(crawlerOptions, scrapped, targetUrl, this.urlValidMs, crawlOpts);
|
const formatted = await this.formatSnapshot(crawlerOptions, scrapped, targetUrl, this.urlValidMs, crawlOpts);
|
||||||
chargeAmount = this.assignChargeAmount(formatted, crawlerOptions);
|
chargeAmount = this.assignChargeAmount(formatted, tierPolicy);
|
||||||
if (crawlerOptions.tokenBudget && chargeAmount > crawlerOptions.tokenBudget) {
|
|
||||||
throw new BudgetExceededError(`Token budget (${crawlerOptions.tokenBudget}) exceeded, intended charge amount ${chargeAmount}.`);
|
|
||||||
}
|
|
||||||
sseStream.write({
|
sseStream.write({
|
||||||
event: 'data',
|
event: 'data',
|
||||||
data: formatted,
|
data: formatted,
|
||||||
@ -379,11 +375,7 @@ export class CrawlerHost extends RPCHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const formatted = await this.formatSnapshot(crawlerOptions, scrapped, targetUrl, this.urlValidMs, crawlOpts);
|
const formatted = await this.formatSnapshot(crawlerOptions, scrapped, targetUrl, this.urlValidMs, crawlOpts);
|
||||||
chargeAmount = this.assignChargeAmount(formatted, crawlerOptions);
|
chargeAmount = this.assignChargeAmount(formatted, tierPolicy);
|
||||||
|
|
||||||
if (crawlerOptions.tokenBudget && chargeAmount > crawlerOptions.tokenBudget) {
|
|
||||||
throw new BudgetExceededError(`Token budget (${crawlerOptions.tokenBudget}) exceeded, intended charge amount ${chargeAmount}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scrapped?.pdfs?.length && !chargeAmount) {
|
if (scrapped?.pdfs?.length && !chargeAmount) {
|
||||||
continue;
|
continue;
|
||||||
@ -405,10 +397,7 @@ export class CrawlerHost extends RPCHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const formatted = await this.formatSnapshot(crawlerOptions, lastScrapped, targetUrl, this.urlValidMs, crawlOpts);
|
const formatted = await this.formatSnapshot(crawlerOptions, lastScrapped, targetUrl, this.urlValidMs, crawlOpts);
|
||||||
chargeAmount = this.assignChargeAmount(formatted, crawlerOptions);
|
chargeAmount = this.assignChargeAmount(formatted, tierPolicy);
|
||||||
if (crawlerOptions.tokenBudget && chargeAmount > crawlerOptions.tokenBudget) {
|
|
||||||
throw new BudgetExceededError(`Token budget (${crawlerOptions.tokenBudget}) exceeded, intended charge amount ${chargeAmount}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatted;
|
return formatted;
|
||||||
}
|
}
|
||||||
@ -434,10 +423,7 @@ export class CrawlerHost extends RPCHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const formatted = await this.formatSnapshot(crawlerOptions, scrapped, targetUrl, this.urlValidMs, crawlOpts);
|
const formatted = await this.formatSnapshot(crawlerOptions, scrapped, targetUrl, this.urlValidMs, crawlOpts);
|
||||||
chargeAmount = this.assignChargeAmount(formatted, crawlerOptions);
|
chargeAmount = this.assignChargeAmount(formatted, tierPolicy);
|
||||||
if (crawlerOptions.tokenBudget && chargeAmount > crawlerOptions.tokenBudget) {
|
|
||||||
throw new BudgetExceededError(`Token budget (${crawlerOptions.tokenBudget}) exceeded, intended charge amount ${chargeAmount}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (crawlerOptions.respondWith === 'screenshot' && Reflect.get(formatted, 'screenshotUrl')) {
|
if (crawlerOptions.respondWith === 'screenshot' && Reflect.get(formatted, 'screenshotUrl')) {
|
||||||
return assignTransferProtocolMeta(`${formatted.textRepresentation}`,
|
return assignTransferProtocolMeta(`${formatted.textRepresentation}`,
|
||||||
@ -465,10 +451,7 @@ export class CrawlerHost extends RPCHost {
|
|||||||
throw new AssertionFailureError(`No content available for URL ${targetUrl}`);
|
throw new AssertionFailureError(`No content available for URL ${targetUrl}`);
|
||||||
}
|
}
|
||||||
const formatted = await this.formatSnapshot(crawlerOptions, lastScrapped, targetUrl, this.urlValidMs, crawlOpts);
|
const formatted = await this.formatSnapshot(crawlerOptions, lastScrapped, targetUrl, this.urlValidMs, crawlOpts);
|
||||||
chargeAmount = this.assignChargeAmount(formatted, crawlerOptions);
|
chargeAmount = this.assignChargeAmount(formatted, tierPolicy);
|
||||||
if (crawlerOptions.tokenBudget && chargeAmount > crawlerOptions.tokenBudget) {
|
|
||||||
throw new BudgetExceededError(`Token budget (${crawlerOptions.tokenBudget}) exceeded, intended charge amount ${chargeAmount}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (crawlerOptions.respondWith === 'screenshot' && Reflect.get(formatted, 'screenshotUrl')) {
|
if (crawlerOptions.respondWith === 'screenshot' && Reflect.get(formatted, 'screenshotUrl')) {
|
||||||
|
|
||||||
@ -840,7 +823,8 @@ export class CrawlerHost extends RPCHost {
|
|||||||
yield this.jsdomControl.narrowSnapshot(draftSnapshot, crawlOpts);
|
yield this.jsdomControl.narrowSnapshot(draftSnapshot, crawlOpts);
|
||||||
}
|
}
|
||||||
let fallbackProxyIsUsed = false;
|
let fallbackProxyIsUsed = false;
|
||||||
if (((!crawlOpts?.allocProxy || crawlOpts.allocProxy === 'none') && !crawlOpts?.proxyUrl) &&
|
if (
|
||||||
|
((!crawlOpts?.allocProxy || crawlOpts.allocProxy !== 'none') && !crawlOpts?.proxyUrl) &&
|
||||||
(analyzed.tokens < 42 || sideLoaded.status !== 200)
|
(analyzed.tokens < 42 || sideLoaded.status !== 200)
|
||||||
) {
|
) {
|
||||||
const proxyLoaded = await this.sideLoadWithAllocatedProxy(urlToCrawl, altOpts);
|
const proxyLoaded = await this.sideLoadWithAllocatedProxy(urlToCrawl, altOpts);
|
||||||
@ -911,18 +895,14 @@ export class CrawlerHost extends RPCHost {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assignChargeAmount(formatted: FormattedPage, crawlerOptions?: CrawlerOptions) {
|
assignChargeAmount(formatted: FormattedPage, saasTierPolicy?: Parameters<typeof this.saasApplyTierPolicy>[0]) {
|
||||||
if (!formatted) {
|
if (!formatted) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let amount = 0;
|
let amount = 0;
|
||||||
if (formatted.content) {
|
if (formatted.content) {
|
||||||
const x1 = estimateToken(formatted.content);
|
amount = estimateToken(formatted.content);
|
||||||
if (crawlerOptions?.respondWith?.toLowerCase().includes('lm')) {
|
|
||||||
amount += x1 * 2;
|
|
||||||
}
|
|
||||||
amount += x1;
|
|
||||||
} else if (formatted.description) {
|
} else if (formatted.description) {
|
||||||
amount += estimateToken(formatted.description);
|
amount += estimateToken(formatted.description);
|
||||||
}
|
}
|
||||||
@ -939,6 +919,10 @@ export class CrawlerHost extends RPCHost {
|
|||||||
amount += 765;
|
amount += 765;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (saasTierPolicy) {
|
||||||
|
amount = this.saasApplyTierPolicy(saasTierPolicy, amount);
|
||||||
|
}
|
||||||
|
|
||||||
Object.assign(formatted, { usage: { tokens: amount } });
|
Object.assign(formatted, { usage: { tokens: amount } });
|
||||||
assignMeta(formatted, { usage: { tokens: amount } });
|
assignMeta(formatted, { usage: { tokens: amount } });
|
||||||
|
|
||||||
@ -1312,4 +1296,54 @@ export class CrawlerHost extends RPCHost {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async saasAssertTierPolicy(opts: CrawlerOptions, auth: JinaEmbeddingsAuthDTO) {
|
||||||
|
let chargeScalar = 1;
|
||||||
|
let minimalCharge = 0;
|
||||||
|
|
||||||
|
if (opts.injectPageScript || opts.injectFrameScript) {
|
||||||
|
await auth.assertTier(0, 'Script injection');
|
||||||
|
minimalCharge = 4_000;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.withGeneratedAlt) {
|
||||||
|
await auth.assertTier(0, 'Alt text generation');
|
||||||
|
minimalCharge = 4_000;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.withIframe) {
|
||||||
|
await auth.assertTier(0, 'Iframe');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.engine === ENGINE_TYPE.CF_BROWSER_RENDERING) {
|
||||||
|
await auth.assertTier(0, 'Cloudflare browser rendering');
|
||||||
|
minimalCharge = 4_000;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.respondWith.includes('lm') || opts.engine?.includes('lm')) {
|
||||||
|
await auth.assertTier(0, 'Language model');
|
||||||
|
minimalCharge = 4_000;
|
||||||
|
chargeScalar = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.proxy && opts.proxy !== 'none') {
|
||||||
|
await auth.assertTier(['auto', 'any'].includes(opts.proxy) ? 0 : 2, 'Proxy allocation');
|
||||||
|
chargeScalar = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
budget: opts.tokenBudget || 0,
|
||||||
|
chargeScalar,
|
||||||
|
minimalCharge,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
saasApplyTierPolicy(policy: Awaited<ReturnType<typeof this.saasAssertTierPolicy>>, chargeAmount: number) {
|
||||||
|
const effectiveChargeAmount = policy.chargeScalar * Math.max(chargeAmount, policy.minimalCharge);
|
||||||
|
if (policy.budget && policy.budget < effectiveChargeAmount) {
|
||||||
|
throw new BudgetExceededError(`Token budget (${policy.budget}) exceeded, intended charge amount ${effectiveChargeAmount}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return effectiveChargeAmount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ import { AsyncLocalContext } from '../services/async-context';
|
|||||||
import envConfig from '../shared/services/secrets';
|
import envConfig from '../shared/services/secrets';
|
||||||
import { JinaEmbeddingsDashboardHTTP } from '../shared/3rd-party/jina-embeddings';
|
import { JinaEmbeddingsDashboardHTTP } from '../shared/3rd-party/jina-embeddings';
|
||||||
import { JinaEmbeddingsTokenAccount } from '../shared/db/jina-embeddings-token-account';
|
import { JinaEmbeddingsTokenAccount } from '../shared/db/jina-embeddings-token-account';
|
||||||
|
import { TierFeatureConstraintError } from '../services/errors';
|
||||||
|
|
||||||
const authDtoLogger = logger.child({ service: 'JinaAuthDTO' });
|
const authDtoLogger = logger.child({ service: 'JinaAuthDTO' });
|
||||||
|
|
||||||
@ -236,6 +237,30 @@ export class JinaEmbeddingsAuthDTO extends AutoCastable {
|
|||||||
return this.user!;
|
return this.user!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async assertTier(n: number, feature?: string) {
|
||||||
|
let user;
|
||||||
|
try {
|
||||||
|
user = await this.assertUser();
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof AuthenticationRequiredError) {
|
||||||
|
throw new AuthenticationRequiredError({
|
||||||
|
message: `Authentication is required to use this feature${feature ? ` (${feature})` : ''}. Please provide a valid API key.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tier = parseInt(user.metadata?.speed_level);
|
||||||
|
if (isNaN(tier) || tier < n) {
|
||||||
|
throw new TierFeatureConstraintError({
|
||||||
|
message: `Your current plan does not support this feature${feature ? ` (${feature})` : ''}. Please upgrade your plan.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
getRateLimits(...tags: string[]) {
|
getRateLimits(...tags: string[]) {
|
||||||
const descs = tags.map((x) => this.user?.customRateLimits?.[x] || []).flat().filter((x) => x.isEffective());
|
const descs = tags.map((x) => this.user?.customRateLimits?.[x] || []).flat().filter((x) => x.isEffective());
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ export class EmailUnverifiedError extends ApplicationError { }
|
|||||||
export class InsufficientCreditsError extends ApplicationError { }
|
export class InsufficientCreditsError extends ApplicationError { }
|
||||||
|
|
||||||
@StatusCode(40202)
|
@StatusCode(40202)
|
||||||
export class FreeFeatureLimitError extends ApplicationError { }
|
export class TierFeatureConstraintError extends ApplicationError { }
|
||||||
|
|
||||||
@StatusCode(40203)
|
@StatusCode(40203)
|
||||||
export class InsufficientBalanceError extends ApplicationError { }
|
export class InsufficientBalanceError extends ApplicationError { }
|
||||||
|
Loading…
x
Reference in New Issue
Block a user