mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-25 07:27:04 +00:00
docs: metering and examples (#1803)
Docker Image CI / build-and-push-image (push) Has been cancelled
Maintain Release Merge PR / update-release-pr (push) Has been cancelled
release-please / release-please (push) Has been cancelled
test / test (20.x) (push) Has been cancelled
test / test (22.x) (push) Has been cancelled
test / api-test (22.x) (push) Has been cancelled
Docker Image CI / build-and-push-image (push) Has been cancelled
Maintain Release Merge PR / update-release-pr (push) Has been cancelled
release-please / release-please (push) Has been cancelled
test / test (20.x) (push) Has been cancelled
test / test (22.x) (push) Has been cancelled
test / api-test (22.x) (push) Has been cancelled
* chore: expose meteringService methods in wrapper while not in an extension * docs: metering and examples
This commit is contained in:
@@ -180,4 +180,5 @@ This repository, including all its contents, sub-projects, modules, and componen
|
||||
## Links to Other READMEs
|
||||
### Backend
|
||||
- [PuterAI Module](./src/backend/doc/modules/puterai/README.md)
|
||||
- [Metering and Billing Service](./src/backend/src/services/MeteringService/README.md)
|
||||
- [Metering Service](./src/backend/src/services/MeteringService/README.md)
|
||||
- [Extensions Development Guide](./extensions/README.md)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Extension System Development Guide
|
||||
TODO: Move extensions docs into here?
|
||||
For now see [here](../doc/contributors/extensions/README.md)
|
||||
|
||||
## Specific Extension Logs
|
||||
+3
-2
@@ -13,12 +13,13 @@ extension.on('metering:registerAvailablePolicies', async (
|
||||
console.warn('WARNING!!! unlimitedUsage is enabled, this is not recommended for production use');
|
||||
event.availablePolicies.push({
|
||||
id: 'unlimited',
|
||||
monthUsageAllowance: 500_000_000 * 100_000_000, // unless you're like, jeff's, mark's and elon's illegitamate son, you probably won't hit $5m a month
|
||||
monthlyStorageAllowance: 100_000 * 1024 * 1024, // 100MiB
|
||||
monthUsageAllowance: 5_000_000 * 1_000_000 * 100, // unless you're like, jeff's, mark's, and elon's illegitamate son, you probably won't hit $5m a month
|
||||
monthlyStorageAllowance: 100_000 * 1024 * 1024, // 100MiB but ignored in local dev
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
extension.on('metering:getUserSubscription', async (/** @type {{actor: import('@heyputer/backend/src/services/auth/Actor').Actor, userSubscriptionId: string}} */event) => {
|
||||
event.userSubscriptionId = event?.actor?.type?.user?.subscription?.active ? event.actor.type.user.subscription?.tier : undefined;
|
||||
// default location for user sub, but can techinically be anywhere else or fetched on request
|
||||
});
|
||||
@@ -12,5 +12,11 @@ export class MeteringServiceWrapper extends BaseService {
|
||||
alarmService: this.services.get('alarm'),
|
||||
eventService: this.services.get('event'),
|
||||
});
|
||||
// TODO DS: if we can pull this to an extension I don't need this
|
||||
// for now this is util so you don't have to extract this.meteringService
|
||||
Object.getOwnPropertyNames(MeteringService.prototype).forEach(fn => {
|
||||
if ( fn === 'constructor' ) return;
|
||||
this[fn] = (...args) => this.meteringService[fn](args);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,55 @@
|
||||
# Metering and Billing Service
|
||||
# Metering Service
|
||||
|
||||
This service provides all metering functionality in puter. It relies on our own KV infrastructure to track usage (note the implementation of kvStore affects performance, and atomicity, currently sqlite implementation is not atomic).
|
||||
This service provides all metering functionality in puter.
|
||||
It relies on our own KV infrastructure to track usage (note the implementation of kvStore affects performance, and atomicity, currently sqlite implementation is not atomic).
|
||||
|
||||
It will also slowly add billing functionlity in it and through extension events.
|
||||
It will also slowly add functionality around credit purchasing in the future, but for now it is just metering and usage.
|
||||
This should be the primary, and ideally only, way to check for usage and record it.
|
||||
|
||||
## Usage
|
||||
### Within Core Modules
|
||||
To use the metering service within core modules, you can access it via the `services` object. Here's an example of how to check if an actor has enough credits for a specific usage type:
|
||||
|
||||
```typescript
|
||||
class SomeCoreModule extends BaseService {
|
||||
get #meteringService(): MeteringService {
|
||||
return this.services.get('meteringService') as MeteringService;
|
||||
}
|
||||
|
||||
async someMeteredFunction(actor: Actor) {
|
||||
const hasEnoughCredits = await this.#meteringService.hasEnoughCreditsFor(actor, 'someUsageKey:units', 1000);
|
||||
|
||||
// ...
|
||||
|
||||
const updatedUsage = await this.#meteringService.incrementUsage(actor, 'someUsageKey:units', 1000);
|
||||
}
|
||||
}
|
||||
```
|
||||
Note you don't have to structure like that if you don't want, but it's a nice way to encapsulate the service access. You can also do:
|
||||
```typescript
|
||||
const meteringService = this.services.get('meteringService') as MeteringService;
|
||||
// or
|
||||
const meteringService = Context.get('services').get('meteringService') as MeteringService;
|
||||
```
|
||||
or any other way you like to access services.
|
||||
### Within Extensions
|
||||
To use the metering service within extensions, you can import it using the extension's import service method
|
||||
|
||||
```javascript
|
||||
/** @type {import('@heyputer/backend/src/services/MeteringService/MeteringServiceWrapper.mjs').MeteringServiceWrapper} */
|
||||
const meteringService = extension.import('service:meteringService');
|
||||
```
|
||||
|
||||
### Note on imports
|
||||
Due to the way we structure services, when importing the metering service in extensions, you get the `MeteringServiceWrapper` class. This is a bit of a middlestep while MeteringService is not an extension itself. Which is why you'll see some places doing:
|
||||
```typescript
|
||||
const meteringService = this.services.get('meteringService').meteringService as MeteringService
|
||||
```
|
||||
but for usability, those same methods are exposed directly on the wrapper so you don't need to do that.
|
||||
|
||||
## Cost maps
|
||||
The metering service relies on cost maps to determine how much to charge for a given operation. Cost maps are simple JSON objects that map a usage type to a cost per unit in microcents (1 millionth of a cent).
|
||||
The metering service relies on cost maps to determine how much to charge for a given operation.
|
||||
Cost maps are simple JSON objects that map a usage type to a cost per unit in microcents (1 millionth of a cent).
|
||||
For example, a cost map for AWS Polly might look like this:
|
||||
|
||||
```json
|
||||
@@ -16,8 +60,30 @@ For example, a cost map for AWS Polly might look like this:
|
||||
```
|
||||
|
||||
We need to manually update these for now until we can automate it somehow.
|
||||
You can add more costs to the cost map as needed.
|
||||
|
||||
## Usage and allowance tracking
|
||||
This service provides functionality to directly check if a user has enough credits to perform an operation, and to record usage after the operation is complete.
|
||||
See [MeteringService.ts](./MeteringService.ts) for more details on how metering works.
|
||||
This should be the primary, and ideally only, way to check for usage and record it.
|
||||
## Cost overrides
|
||||
In some cases, you may want to override the default cost for a specific actor, or give a cost if not provided in the cost map.
|
||||
you can do this by passing in the cost override when incrementing usage:
|
||||
|
||||
```typescript
|
||||
await meteringService.incrementUsage(actor, 'someUnmappedOperation:units', 1000, 5000000); // override cost to 5 cents = 5 million microcents for the whole 1000 units
|
||||
```
|
||||
|
||||
## Other util methods
|
||||
See [MeteringService.ts](./MeteringService.ts) for more details on how metering works. Its all typescript so you can always just get intellisense on the methods.
|
||||
|
||||
|
||||
## Adding and Getting User Subscription Plans
|
||||
Though the metering service itself doesn't handle subscriptions nor credit purchases (yet at least), it does emit events for extensions to provide them with the necessary data to limit usage for users.
|
||||
These following events are emitted:
|
||||
- `metering:overrideDefaultSubscription` - allows extension to override the default subscription plan for a user
|
||||
- `metering:registerAvailablePolicies` - allows extension to register available subscription policies/plans
|
||||
- `metering:getUserSubscription` - allows extension to provide the current subscription plan for a user
|
||||
For example on these see the extension [meteringAndBilling](../../../../../extensions/meteringAndBilling/eventListeners/subscriptionEvents.js) for how to use these events to provide subscription plans.
|
||||
|
||||
## Examples
|
||||
### Core Module example
|
||||
See OpenAI module for an example of how to use the metering service within a core module: [OpenAICompletionService.mjs](../../modules/puterai/OpenAiCompletionService/OpenAICompletionService.mjs)
|
||||
### Extension example
|
||||
See meteringAndBilling extension for an example of how to use the metering service within an extension: [usage.js](../../../../../extensions/meteringAndBilling/routes/usage.js)
|
||||
Reference in New Issue
Block a user