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

* chore: expose meteringService methods in wrapper while not in an extension

* docs: metering and examples
This commit is contained in:
Daniel Salazar
2025-10-21 17:25:46 -07:00
committed by GitHub
parent 16fd614b84
commit 9394b5e204
9 changed files with 90 additions and 11 deletions
+2 -1
View File
@@ -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)
+5
View File
@@ -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
@@ -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)