*/ public array $backoff = [20, 60, 120]; public int $timeout = 60; public function __construct( private readonly int $requestId, private readonly string $providerTaskId, private readonly int $pollAttempt = 1, ) { $this->onQueue((string) config('ai-editing.queue.name', 'default')); } public function handle( AiImageProviderManager $providers, AiSafetyPolicyService $safetyPolicy, AiEditingRuntimeConfig $runtimeConfig, AiUsageLedgerService $usageLedger ): void { $request = AiEditRequest::query()->with('outputs')->find($this->requestId); if (! $request || $request->status !== AiEditRequest::STATUS_PROCESSING) { return; } $run = AiProviderRun::query()->create([ 'request_id' => $request->id, 'provider' => $request->provider, 'attempt' => ((int) $request->providerRuns()->max('attempt')) + 1, 'provider_task_id' => $this->providerTaskId, 'status' => AiProviderRun::STATUS_RUNNING, 'started_at' => now(), ]); $result = $providers->forProvider($request->provider)->poll($request, $this->providerTaskId); $run->forceFill([ 'status' => $result->status === 'succeeded' ? AiProviderRun::STATUS_SUCCEEDED : ($result->status === 'processing' ? AiProviderRun::STATUS_RUNNING : AiProviderRun::STATUS_FAILED), 'http_status' => $result->httpStatus, 'finished_at' => $result->status === 'processing' ? null : now(), 'duration_ms' => $run->started_at ? (int) max(0, $run->started_at->diffInMilliseconds(now())) : null, 'cost_usd' => $result->costUsd, 'request_payload' => $result->requestPayload, 'response_payload' => $result->responsePayload, 'error_message' => $result->failureMessage, ])->save(); if ($result->status === 'succeeded') { $outputDecision = $safetyPolicy->evaluateProviderOutput($result); if ($outputDecision->blocked) { $request->forceFill([ 'status' => AiEditRequest::STATUS_BLOCKED, 'safety_state' => $outputDecision->state, 'safety_reasons' => $outputDecision->reasonCodes, 'failure_code' => $outputDecision->failureCode ?? 'output_policy_blocked', 'failure_message' => $outputDecision->failureMessage, 'completed_at' => now(), ])->save(); return; } foreach ($result->outputs as $output) { AiEditOutput::query()->updateOrCreate( [ 'request_id' => $request->id, 'provider_asset_id' => (string) Arr::get($output, 'provider_asset_id', $this->providerTaskId), ], [ 'provider_url' => Arr::get($output, 'provider_url'), 'mime_type' => Arr::get($output, 'mime_type'), 'width' => Arr::get($output, 'width'), 'height' => Arr::get($output, 'height'), 'is_primary' => true, 'safety_state' => 'passed', 'safety_reasons' => [], 'generated_at' => now(), 'metadata' => ['provider' => $request->provider], ] ); } $request->forceFill([ 'status' => AiEditRequest::STATUS_SUCCEEDED, 'safety_state' => 'passed', 'safety_reasons' => [], 'failure_code' => null, 'failure_message' => null, 'completed_at' => now(), ])->save(); $usageLedger->recordDebitForRequest($request->fresh(), $result->costUsd, [ 'source' => 'poll_job', 'poll_attempt' => $this->pollAttempt, ]); return; } if ($result->status === 'processing') { $maxPolls = $runtimeConfig->maxPolls(); if ($this->pollAttempt < $maxPolls) { self::dispatch($request->id, $this->providerTaskId, $this->pollAttempt + 1) ->delay(now()->addSeconds(20)) ->onQueue($runtimeConfig->queueName()); } return; } $request->forceFill([ 'status' => $result->status === 'blocked' ? AiEditRequest::STATUS_BLOCKED : AiEditRequest::STATUS_FAILED, 'safety_state' => $result->safetyState ?? $request->safety_state, 'safety_reasons' => $result->safetyReasons !== [] ? $result->safetyReasons : $request->safety_reasons, 'failure_code' => $result->failureCode, 'failure_message' => $result->failureMessage, 'completed_at' => now(), ])->save(); } }