Files
puter/src/backend/drivers/decorators.ts
T

198 lines
6.9 KiB
TypeScript

/*
* Copyright (C) 2024-present Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {
DRIVER_CONCURRENT_KEY,
DRIVER_DEFAULT_KEY,
DRIVER_INTERFACE_KEY,
DRIVER_NAME_KEY,
DRIVER_NO_USER_SESSION_KEY,
DRIVER_RATE_LIMIT_KEY,
DRIVER_REQUIRE_REPUTATION_KEY,
DRIVER_REQUIRE_SUBSCRIPTION_KEY,
validateDriverConcurrent,
validateDriverRateLimit,
validateDriverRequireReputation,
validateDriverRequireSubscription,
type DriverConcurrentConfig,
type DriverRateLimitConfig,
type DriverRequireReputationConfig,
type DriverRequireSubscriptionConfig,
} from './meta';
/** Options for the `@Driver` class decorator. */
export interface DriverOptions {
/**
* Unique name for this implementation within its interface. Defaults to the
* class name.
*/
name?: string;
/** When true, this driver is the default for its interface. */
default?: boolean;
/**
* Rate-limit policy. Each driver method can declare its own limit / window
* / storage backend; methods not listed inherit `default`, and undeclared
* methods fall through to the global driver default (600/min in
* `checkDriverRateLimit`).
*
* ```ts
* @Driver('puter-kvstore', {
* rateLimit: {
* default: { limit: 600, window: 60_000 },
* methods: {
* list: { limit: 60, window: 60_000, backend: 'kv' },
* set: { limit: 200, window: 60_000, backend: 'redis' },
* },
* },
* })
* ```
*/
rateLimit?: DriverRateLimitConfig;
/**
* Concurrent in-flight policy. Same envelope as `rateLimit` minus `window`.
* Adds `bySubscription` to scale the cap by subscription tier
* (`SubscriptionPolicy.id` from MeteringService).
*
* ```ts
* @Driver('puter-chat-completion', {
* concurrent: {
* default: { limit: 5, backend: 'redis' },
* methods: {
* complete: {
* limit: 5,
* bySubscription: { user_free: 1, unlimited: 50 },
* backend: 'redis',
* },
* },
* },
* })
* ```
*
* Methods that don't appear in either `default` or `methods` are unbounded
* — matching today's behaviour where nothing is gated.
*/
concurrent?: DriverConcurrentConfig;
/**
* When true, `/drivers/call` rejects bare account-session ("root") tokens
* for this driver — callers need an app/worker token or a dashboard-minted
* API token. See `DriverMeta.noUserSession`.
*/
noUserSession?: boolean;
/**
* Subscriber-only methods. `true` accepts any non-free plan; an array of
* `SubscriptionPolicy.id`s accepts only those. Methods listed in neither
* `methods` nor `default` stay open to every plan — this is opt-in.
*
* ```ts
* @Driver('puter-video-generation', {
* requireSubscription: {
* methods: {
* generate: true,
* generateLong: ['business', 'pro'],
* },
* },
* })
* ```
*/
requireSubscription?: DriverRequireSubscriptionConfig;
/**
* Methods that need a trusted-enough account. Each entry names a tier; what
* the tier takes is deployment config (`reputationGate.tiers`), so a driver
* declares the standard it holds a method to and not the number. Methods
* listed in neither `methods` nor `default` ask for no floor — this is
* opt-in.
*
* ```ts
* @Driver('puter-chat-completion', {
* requireReputation: {
* methods: { complete: 'standard' },
* },
* })
* ```
*/
requireReputation?: DriverRequireReputationConfig;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyCtor = new (...args: any[]) => any;
/**
* Class decorator that marks a driver implementation and records its interface
*
* - Name on the prototype.
*
* Equivalent imperative approach (no decorator needed):
*
* ```ts
* class MyDriver extends PuterDriver {
* readonly driverInterface = 'puter-chat-completion';
* readonly driverName = 'my-impl';
* readonly isDefault = true;
* }
* ```
*
* Usage:
*
* ```ts
* @Driver('puter-chat-completion', { name: 'openai-completion', default: true })
* class OpenAIChatDriver extends PuterDriver {
* async complete(args) { ... }
* }
* ```
*/
export function Driver(interfaceName: string, opts: DriverOptions = {}) {
// Validate eagerly at decoration time so a malformed rateLimit /
// concurrent block surfaces during module load — not when the first
// request hits the route and the controller resolves driver meta.
const label = `@Driver('${interfaceName}'${opts.name ? `, name='${opts.name}'` : ''})`;
const rateLimit =
opts.rateLimit !== undefined
? validateDriverRateLimit(opts.rateLimit, label)
: undefined;
const concurrent =
opts.concurrent !== undefined
? validateDriverConcurrent(opts.concurrent, label)
: undefined;
const requireSubscription =
opts.requireSubscription !== undefined
? validateDriverRequireSubscription(opts.requireSubscription, label)
: undefined;
const requireReputation =
opts.requireReputation !== undefined
? validateDriverRequireReputation(opts.requireReputation, label)
: undefined;
return <T extends AnyCtor>(
value: T,
_context: ClassDecoratorContext<T>,
): void => {
const proto = value.prototype as Record<string, unknown>;
proto[DRIVER_INTERFACE_KEY] = interfaceName;
proto[DRIVER_NAME_KEY] = opts.name ?? value.name;
proto[DRIVER_DEFAULT_KEY] = opts.default ?? false;
if (rateLimit) proto[DRIVER_RATE_LIMIT_KEY] = rateLimit;
if (concurrent) proto[DRIVER_CONCURRENT_KEY] = concurrent;
if (requireSubscription)
proto[DRIVER_REQUIRE_SUBSCRIPTION_KEY] = requireSubscription;
if (requireReputation)
proto[DRIVER_REQUIRE_REPUTATION_KEY] = requireReputation;
if (opts.noUserSession) proto[DRIVER_NO_USER_SESSION_KEY] = true;
};
}