2026-09-17 02:34:43 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Models;
|
|
|
|
|
|
2026-09-18 18:41:33 +00:00
|
|
|
use App\Support\Suites;
|
2026-09-17 02:34:43 +00:00
|
|
|
use Database\Factories\BenchmarkRunFactory;
|
|
|
|
|
use Illuminate\Database\Eloquent\Builder;
|
2026-09-18 18:41:33 +00:00
|
|
|
use Illuminate\Database\Eloquent\Collection;
|
2026-09-17 02:34:43 +00:00
|
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
|
|
|
use Illuminate\Database\Eloquent\Model;
|
|
|
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
|
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
|
|
|
|
|
|
|
|
class BenchmarkRun extends Model
|
|
|
|
|
{
|
|
|
|
|
/** @use HasFactory<BenchmarkRunFactory> */
|
|
|
|
|
use HasFactory;
|
|
|
|
|
|
|
|
|
|
public const VISIBILITY_PUBLIC = 'public';
|
|
|
|
|
|
|
|
|
|
public const VISIBILITY_PRIVATE = 'private';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Columns written only at submission/import time. Everything here is
|
|
|
|
|
* immutable once stored; user updates may only touch $overlay.
|
|
|
|
|
*/
|
|
|
|
|
protected $fillable = [
|
|
|
|
|
'run_id', 'ran_at', 'suite', 'run_key',
|
|
|
|
|
'model_id', 'model_quant', 'model_quant_raw', 'model_path',
|
|
|
|
|
'engine', 'engine_build', 'n_ctx', 'slots',
|
2026-09-19 16:56:40 +00:00
|
|
|
'gpu', 'gpu_count', 'gpu_id', 'gpu_source', 'host', 'client', 'endpoint',
|
2026-09-17 02:34:43 +00:00
|
|
|
'label', 'config', 'image_digest', 'validators', 'suite_versions',
|
|
|
|
|
'harness', 'harness_ver', 'harness_mode', 'user_id',
|
|
|
|
|
'visibility', 'notes',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/** The only fields a user may change after submission. */
|
2026-09-19 16:56:40 +00:00
|
|
|
/**
|
|
|
|
|
* The only fields a user may change after submission. gpu/gpu_id are
|
|
|
|
|
* included deliberately: confirming a run's GPU against the canonical
|
|
|
|
|
* catalog is owner behavior (it links identity, it does not alter
|
|
|
|
|
* benchmark metrics).
|
|
|
|
|
*/
|
|
|
|
|
public const OVERLAY_FIELDS = ['visibility', 'notes', 'gpu', 'gpu_id'];
|
2026-09-17 02:34:43 +00:00
|
|
|
|
|
|
|
|
protected function casts(): array
|
|
|
|
|
{
|
|
|
|
|
return [
|
|
|
|
|
'ran_at' => 'datetime',
|
|
|
|
|
'validators' => 'array',
|
|
|
|
|
'suite_versions' => 'array',
|
|
|
|
|
'n_ctx' => 'integer',
|
|
|
|
|
'slots' => 'integer',
|
|
|
|
|
'gpu_count' => 'integer',
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Submitted metrics are immutable: any update that touches a column
|
|
|
|
|
* outside OVERLAY_FIELDS is silently reverted before it hits the
|
|
|
|
|
* database. This is the safety net for every user-facing update path.
|
|
|
|
|
*/
|
|
|
|
|
protected static function booted(): void
|
|
|
|
|
{
|
|
|
|
|
static::updating(function (self $run): void {
|
|
|
|
|
foreach ($run->getDirty() as $key => $_) {
|
|
|
|
|
if (! in_array($key, self::OVERLAY_FIELDS, true)) {
|
|
|
|
|
$run->setAttribute($key, $run->getOriginal($key));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function user(): BelongsTo
|
|
|
|
|
{
|
|
|
|
|
return $this->belongsTo(User::class);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-18 18:41:33 +00:00
|
|
|
public function softbenchResults(): HasMany
|
2026-09-17 02:34:43 +00:00
|
|
|
{
|
2026-09-18 18:41:33 +00:00
|
|
|
return $this->hasMany(SoftbenchResult::class);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function infrabenchResults(): HasMany
|
|
|
|
|
{
|
|
|
|
|
return $this->hasMany(InfrabenchResult::class);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function prosebenchResults(): HasMany
|
|
|
|
|
{
|
|
|
|
|
return $this->hasMany(ProsebenchResult::class);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function psychbenchResults(): HasMany
|
|
|
|
|
{
|
|
|
|
|
return $this->hasMany(PsychbenchResult::class);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function scenarioRuns(): HasMany
|
|
|
|
|
{
|
|
|
|
|
return $this->hasMany(ScenarioRun::class);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The suite-specific result rows for this run, via the registry. Returns
|
|
|
|
|
* an empty collection for suites this run does not have.
|
|
|
|
|
*/
|
|
|
|
|
public function suiteResults(): Collection
|
|
|
|
|
{
|
|
|
|
|
$model = Suites::modelFor($this->suite);
|
|
|
|
|
|
|
|
|
|
if ($model === null) {
|
2026-09-18 18:49:09 +00:00
|
|
|
return new Collection;
|
2026-09-18 18:41:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$relation = Suites::runRelation($this->suite);
|
|
|
|
|
|
|
|
|
|
return $this->{$relation}()->orderBy('task')->orderBy('id')->get();
|
2026-09-17 02:34:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function isOwnedBy(?User $user): bool
|
|
|
|
|
{
|
|
|
|
|
return $user !== null && $this->user_id === $user->id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Runs visible to $user: everything public, plus the user's own private
|
|
|
|
|
* runs. Applied as a local scope so every listing query starts from it.
|
|
|
|
|
*/
|
|
|
|
|
public function scopeVisibleTo(Builder $query, ?User $user): Builder
|
|
|
|
|
{
|
2026-09-17 02:41:12 +00:00
|
|
|
return $query->where(self::visibleClause($user));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The visibility predicate as a standalone closure, so joined queries
|
2026-09-18 18:49:09 +00:00
|
|
|
* (e.g. the combined results table) can apply the exact same rule
|
|
|
|
|
* without duplicating the logic. Accepts either builder flavor: Eloquent
|
|
|
|
|
* scopes pass an Eloquent Builder, DB::table queries pass a Query Builder.
|
2026-09-17 02:41:12 +00:00
|
|
|
*
|
2026-09-18 18:49:09 +00:00
|
|
|
* @return \Closure(\Illuminate\Database\Query\Builder|Builder): mixed
|
2026-09-17 02:41:12 +00:00
|
|
|
*/
|
|
|
|
|
public static function visibleClause(?User $user): \Closure
|
|
|
|
|
{
|
2026-09-18 18:49:09 +00:00
|
|
|
return function ($q) use ($user) {
|
2026-09-17 02:34:43 +00:00
|
|
|
$q->where('visibility', self::VISIBILITY_PUBLIC);
|
|
|
|
|
|
|
|
|
|
if ($user !== null) {
|
2026-09-18 18:49:09 +00:00
|
|
|
$q->orWhere(fn ($own) => $own
|
2026-09-17 02:34:43 +00:00
|
|
|
->where('visibility', self::VISIBILITY_PRIVATE)
|
|
|
|
|
->where('user_id', $user->id));
|
|
|
|
|
}
|
2026-09-17 02:41:12 +00:00
|
|
|
};
|
2026-09-17 02:34:43 +00:00
|
|
|
}
|
|
|
|
|
}
|