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.
74 lines
2.2 KiB
PHP
74 lines
2.2 KiB
PHP
<?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);
|
|
}
|
|
});
|
|
}
|
|
}
|