Skip to content

Commit 8a0c1ab

Browse files
author
Sebastian BURGIN-FIX (ext)
committed
WIP
1 parent 897ae7c commit 8a0c1ab

5 files changed

Lines changed: 56 additions & 10 deletions

File tree

app/Actions/StoreLlmUsageAction.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@
77
use App\Models\AiModel;
88
use App\Models\AiModelDailyUsage;
99
use Illuminate\Support\Collection;
10-
use Illuminate\Support\Facades\Artisan;
1110
use Illuminate\Support\Facades\Cache;
12-
use Spatie\ResponseCache\Commands\ClearCommand;
1311

1412
class StoreLlmUsageAction
1513
{
@@ -35,10 +33,12 @@ public function store(Collection $rows): int
3533
update: ['ai_model_id', 'prompt_tokens', 'completion_tokens', 'total_tokens', 'requests', 'spend'],
3634
);
3735

36+
// Bumping the version invalidates every cached stats query at once, which is
37+
// all this action is responsible for. Clearing the rendered pages is the
38+
// sync's job, not one day's — FetchLlmAnalyticsCommand does it once the whole
39+
// batch has finished, rather than four times per hourly run.
3840
Cache::increment(LlmUsageStatsAction::VERSION_CACHE_KEY);
3941

40-
Artisan::call(ClearCommand::class);
41-
4242
return $count;
4343
}
4444
}

app/Console/Commands/FetchLlmAnalyticsCommand.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
use Carbon\CarbonImmutable;
99
use Carbon\CarbonInterface;
1010
use Carbon\CarbonPeriod;
11+
use Illuminate\Bus\Batch;
1112
use Illuminate\Console\Command;
13+
use Illuminate\Support\Facades\Bus;
14+
use Spatie\ResponseCache\Facades\ResponseCache;
1215

1316
class FetchLlmAnalyticsCommand extends Command
1417
{
@@ -35,7 +38,19 @@ public function handle(): int
3538

3639
$days = collect(CarbonPeriod::create($from, $to)->toArray());
3740

38-
$days->each(fn (CarbonInterface $day) => FetchLlmUsageJob::dispatch(CarbonImmutable::instance($day)));
41+
// Batched rather than dispatched one by one: the usage figures are rendered
42+
// into the AI pages, so the cached HTML has to go once the numbers change.
43+
// finally() fires after the last day is stored — clearing per job would wipe
44+
// the whole site's response cache once per day in the window, and clearing
45+
// here in the command would fire before the queue had done any work at all.
46+
Bus::batch(
47+
$days->map(fn (CarbonInterface $day): FetchLlmUsageJob => new FetchLlmUsageJob(CarbonImmutable::instance($day)))->all()
48+
)
49+
->name('llm-usage-sync')
50+
->finally(function (Batch $batch): void {
51+
ResponseCache::clear();
52+
})
53+
->dispatch();
3954

4055
$this->info("Dispatched {$days->count()} job(s) for {$from->toDateString()} to {$to->toDateString()}.");
4156

app/Jobs/FetchLlmUsageJob.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
use App\Actions\FetchLlmUsageAction;
88
use App\Actions\StoreLlmUsageAction;
99
use Carbon\CarbonImmutable;
10+
use Illuminate\Bus\Batchable;
1011
use Illuminate\Contracts\Queue\ShouldQueue;
1112
use Illuminate\Foundation\Queue\Queueable;
1213

1314
class FetchLlmUsageJob implements ShouldQueue
1415
{
16+
use Batchable;
1517
use Queueable;
1618

1719
public int $tries = 3;

database/seeders/DatabaseSeeder.php

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,9 @@ public function run(): void
4848
$this->call(AiModelsTableSeeder::class);
4949
$this->call(NetworksTableSeeder::class);
5050
$this->call(NetworkUsersTableSeeder::class);
51+
$this->call(AiModelDailyUsagesTableSeeder::class);
5152

52-
if (app()->isLocal()) {
53-
$this->call(AiModelDailyUsagesTableSeeder::class);
54-
55-
Artisan::call(ClearCommand::class);
56-
}
53+
Artisan::call(ClearCommand::class);
54+
Artisan::call('responsecache:clear');
5755
}
5856
}

tests/Feature/Jobs/FetchLlmUsageJobTest.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Carbon\CarbonImmutable;
1111
use Illuminate\Support\Facades\Cache;
1212
use Illuminate\Support\Facades\Http;
13+
use Spatie\ResponseCache\Facades\ResponseCache;
1314

1415
use function Pest\Laravel\assertDatabaseHas;
1516

@@ -76,3 +77,33 @@ function spendLogsPayload(int $promptTokens): array
7677

7778
expect(Cache::get(LlmUsageStatsAction::VERSION_CACHE_KEY))->toBeGreaterThan($versionBefore);
7879
})->group('llm-analytics');
80+
81+
it('does not clear the response cache when a single day is stored', function () {
82+
$responseCache = ResponseCache::spy();
83+
84+
(new StoreLlmUsageAction)->store(collect([[
85+
'date' => '2026-07-20',
86+
'model' => 'qwen3.6:35b',
87+
'prompt_tokens' => 100,
88+
'completion_tokens' => 50,
89+
'total_tokens' => 150,
90+
'requests' => 1,
91+
'spend' => 0.5,
92+
]]));
93+
94+
// Clearing the whole site's rendered HTML is the sync's decision, not one day's —
95+
// otherwise an hourly run wipes the response cache once per day in its window.
96+
$responseCache->shouldNotHaveReceived('clear');
97+
})->group('llm-analytics');
98+
99+
it('clears the response cache once after the whole sync batch finishes', function () {
100+
Http::fake(['llm.codebar.net/spend/logs*' => Http::response(spendLogsPayload(promptTokens: 100))]);
101+
102+
$responseCache = ResponseCache::spy();
103+
104+
runArtisan('llm:fetch-analytics', ['--from' => '2026-07-20', '--to' => '2026-07-23'])
105+
->assertSuccessful();
106+
107+
// Four days were synced; the rendered pages are dropped exactly once.
108+
$responseCache->shouldHaveReceived('clear')->once();
109+
})->group('llm-analytics');

0 commit comments

Comments
 (0)