feat: Configure AI usage counter - #11
Conversation
Code Review SummaryThis PR configures the AI usage counter by integrating the 🚀 Key Improvements
💡 Minor Suggestions
🚨 Critical Issues
|
35627ef to
ea97334
Compare
ea97334 to
d97f1f4
Compare
nfebe
left a comment
There was a problem hiding this comment.
This looks good but since we have multiple projects that the question of tokens will come up. I built and publish a package that will simplify things there is already a task to implement it in the core and it may remove the need for one table you have here.
See: https://github.com/whilesmartphp/eloquent-agent-metrics
|
@nfebe what's the issue number on trakli core? I can't find it |
|
I already implemented it so its closed |
With this plugin enabled the gate is answered from the owner's plan instead of allowing everything: features come from the plan the Stripe webhook recorded, and wallet and category limits come with it. Turning freemode on leaves the permissive default alone, so enforcement is one switch. An owner with no subscription is treated as being on the free plan. Without that they would be refused every feature while their limits read as unlimited, which is exactly backwards for a free tier. The plan a request is gated against is read from the subscription rows rather than from the payment provider, so no plan cache is kept and nothing has to be invalidated when a subscription changes. The AI allowance is carried on the plan but nothing counts against it yet; that needs a usage meter and is not in this change.
# Conflicts: # src/CloudServiceProvider.php # Conflicts: # src/CloudServiceProvider.php
5c0d2c0 to
b89795f
Compare
nfebe
left a comment
There was a problem hiding this comment.
There is already a way to measure the usage in core
| $table->unsignedBigInteger('user_id'); | ||
| $table->morphs('owner'); | ||
| $table->timestamp('period_start'); | ||
| $table->integer('tokens_used'); |
There was a problem hiding this comment.
Could you add this to the webservice and see how we can eliminate this table?
https://github.com/whilesmartphp/eloquent-agent-metrics
Was this solved?
There was a problem hiding this comment.
I'm still working on this and the other open pr.
There was a problem hiding this comment.
So I already integrated the metrics tracking in the core : https://github.com/trakli/webservice/blob/dev/composer.json#L26
So you just need to used it and remove the custom tracking tables
|
This doesn't load against current Dropping the counter table is addressed, and
This should be a class TokenMeterUsage implements UsageMeter
{
public function remaining(?Model $owner, string $meter, int|float $allowance): int|float
{
if ($meter !== 'ai_tokens' || $owner === null || is_infinite($allowance)) {
return INF;
}
return max(0, $allowance - $owner->tokensUsed(now()->startOfMonth()));
}
public function consume(?Model $owner, string $meter, int $amount): void
{
// core already records through TokenMeter
}
}That needs plans carrying |
|
@nfebe are we taking away the cloudplans.php config? I see that most of the information in the config is in the Eloquent Entitlements plans table |
| /** | ||
| * Determine if the owner is allowed to use a given feature. | ||
| */ | ||
| public function allows(?Model $owner, string $feature): bool |
There was a problem hiding this comment.
The allows method is currently hardcoded to return true, which bypasses all plan-based feature restrictions. This should delegate to the check method to ensure that entitlements are properly enforced across the application.
| public function allows(?Model $owner, string $feature): bool | |
| public function allows(?Model $owner, string $feature): bool | |
| { | |
| return $this->check($owner, $feature)->allowed(); | |
| } |
| $planCode = $this->getPlanCode($owner); | ||
| $plan = config("cloudplans.plans.{$planCode}"); | ||
|
|
||
| // Check if the feature is listed in the plan's features or permissions |
There was a problem hiding this comment.
The features array in config/cloudplans.php contains human-readable strings for the UI (e.g., 'Up to 3 wallets'). Checking for a programmatic feature key (like 'ai_chat') against this array will always fail. You should introduce a machine-readable permissions array in the config or map feature keys to these strings.
| // Check if the feature is listed in the plan's features or permissions | |
| // Suggested: Use a dedicated permissions array for programmatic keys | |
| $permissions = $plan['permissions'] ?? []; | |
| if ($plan && in_array($feature, $permissions, true)) { | |
| return AccessResult::allow($feature); | |
| } |
| { | ||
| $subscription = $this->activeSubscriptionFor($owner); | ||
| if ($subscription && $subscription->plan) { | ||
| return explode('-', $subscription->plan->key)[0]; // gets monthly from monthly-eu |
There was a problem hiding this comment.
Determining the plan code by splitting the key string is brittle. If a plan key doesn't follow the exact prefix-suffix format (e.g., a three-part key), this logic might return an incorrect value. Consider using a more robust mapping or Laravel's Str::before helper.
| return explode('-', $subscription->plan->key)[0]; // gets monthly from monthly-eu | |
| return (string) \Illuminate\Support\Str::before($subscription->plan->key, '-'); |
|
Yes. We should remove the plan definitions from Configuration should retain only deployment flags and presentation copy that are not plan data. This PR should bind a |
|
Hi @nfebe I see this that the changes here What is left to complete the work on that branch? |
|
Can you link the PR instead of the branch? I am not on my desk maybe I can approve it. |
|
I don't know if the work you have done there is complete or is still in progress. YOu haven't created a PR for it yet |
|
I have created a PR for you => #13 |
…eat/ai-allowance # Conflicts: # src/CloudServiceProvider.php
A chat turn now spends from the token allowance the owner's plan carries, and a turn attempted past that allowance answers with a quota message instead of calling the model. The count comes from the usage core already records for every turn, so the plugin keeps no counter of its own and the two cannot drift apart. The allowance resets at the start of each calendar month whatever the billing interval, so a yearly plan gets its allowance back monthly. Features and limits are answered by the shared entitlements layer over the same plan, so the plugin no longer carries its own copy of that logic. Turning freemode on leaves metering off along with the rest of enforcement.
| { | ||
| private const METER = 'ai_tokens'; | ||
|
|
||
| public function remaining(?Model $owner, string $meter, int|float $allowance): int|float |
There was a problem hiding this comment.
Returning INF when the meter name doesn't match is a 'fail open' bug. If a plan defines a limit for a different meter (e.g., 'api_calls'), this implementation will return infinity, effectively bypassing enforcement for that feature. It should instead return the provided $allowance, assuming zero usage has been recorded by this specific provider.
| public function remaining(?Model $owner, string $meter, int|float $allowance): int|float | |
| public function remaining(?Model $owner, string $meter, int|float $allowance): int|float | |
| { | |
| if (is_infinite($allowance)) { | |
| return INF; | |
| } | |
| if ($meter !== self::METER) { | |
| return $allowance; | |
| } |
| $plan->name = $name; | ||
| $plan->features = $this->features($source); | ||
| $plan->limits = $source['limits'] ?? []; | ||
| $plan->meters = ['ai_tokens' => (int) ($source['token_allowance'] ?? 0)]; |
There was a problem hiding this comment.
Aggressively casting token_allowance to (int) prevents the use of null to represent an unlimited allowance in the database. Combined with the (int) cast in the config file, it makes 'unlimited tokens' impossible to configure despite the is_infinite check in the meter logic.
| $plan->meters = ['ai_tokens' => (int) ($source['token_allowance'] ?? 0)]; | |
| $plan->meters = ['ai_tokens' => $source['token_allowance'] ?? 0]; |
A meter this plugin does not count was reported as unlimited rather than as untouched, so a plan that allowed a fixed number of anything else would have been unenforceable. A plan can now state an unlimited token allowance as null, the way its limits already do. Leaving the allowance out still means none.
Fixes #7
Depends on #10