Initial commit: Runware-powered content generator foundation
Laravel app with a cost-tracked Runware.ai integration (image, video, text generation), a hard monthly EUR budget cap with live USD/EUR FX conversion, and a locally-maintained model/pricing table since Runware's model-search API doesn't expose pricing.
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Runware;
|
||||
|
||||
use App\Exceptions\BudgetExceededException;
|
||||
use App\Models\Generation;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Enforces the hard monthly EUR spend cap on Runware usage. Every generation
|
||||
* must go through ensureCanSpend() before the API is called, and the actual
|
||||
* cost must be reconciled afterward via recordActualUsd() so the running
|
||||
* total reflects reality (Runware only bills successful generations).
|
||||
*/
|
||||
class BudgetGuard
|
||||
{
|
||||
protected const LOCK_KEY = 'runware:budget-guard-lock';
|
||||
|
||||
protected const LOCK_TIMEOUT_SECONDS = 10;
|
||||
|
||||
public function __construct(protected CurrencyRateService $rates) {}
|
||||
|
||||
public function capEur(): float
|
||||
{
|
||||
return (float) config('runware.monthly_cap_eur');
|
||||
}
|
||||
|
||||
public function spentEurThisMonth(): float
|
||||
{
|
||||
return (float) Generation::query()
|
||||
->where('created_at', '>=', now()->startOfMonth())
|
||||
->whereNotNull('cost_eur')
|
||||
->sum('cost_eur');
|
||||
}
|
||||
|
||||
public function remainingEur(): float
|
||||
{
|
||||
return max(0.0, $this->capEur() - $this->spentEurThisMonth());
|
||||
}
|
||||
|
||||
public function warnThresholdReached(): bool
|
||||
{
|
||||
$cap = $this->capEur();
|
||||
|
||||
if ($cap <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ($this->spentEurThisMonth() / $cap) >= (float) config('runware.warn_threshold');
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws BudgetExceededException if spending $estimatedUsd would push
|
||||
* this month's total (plus a safety margin) past the cap. Guards the
|
||||
* check with an atomic lock so concurrent requests can't both slip
|
||||
* through right at the boundary.
|
||||
*/
|
||||
public function ensureCanSpend(float $estimatedUsd): void
|
||||
{
|
||||
Cache::lock(self::LOCK_KEY, self::LOCK_TIMEOUT_SECONDS)->block(5, function () use ($estimatedUsd) {
|
||||
$rate = $this->rates->usdToEur();
|
||||
$estimatedEur = $estimatedUsd * $rate;
|
||||
$spent = $this->spentEurThisMonth();
|
||||
$cap = $this->capEur();
|
||||
$margin = $cap * (float) config('runware.safety_margin');
|
||||
|
||||
if ($spent + $estimatedEur + $margin > $cap) {
|
||||
throw new BudgetExceededException($spent, $cap, $estimatedEur);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user