getMessage(), $e->status, $e->errorCode, $e->extra); } catch (Throwable $e) { error_log('timeline-stream preparation error: ' . $e->getMessage()); dbnToolsError('The tool could not prepare this request.', 500, 'internal_error'); } // Only switch to SSE mode once auth + credit checks pass. header('Content-Type: text/event-stream'); header('Cache-Control: no-cache, no-transform'); header('X-Accel-Buffering: no'); while (ob_get_level() > 0) ob_end_flush(); ob_implicit_flush(true); function sseEmit(string $event, array $data): void { echo "event: {$event}\n"; echo 'data: ' . json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n\n"; if (function_exists('flush')) @flush(); } $start = microtime(true); try { $engine = (string)$timelineRoute['effective_engine']; if (!empty($timelineRoute['auto_upgraded_engine'])) { $label = match ($engine) { 'azure_full' => 'Deep', 'azure_mini' => 'Standard', default => 'Quick', }; sseEmit('status', [ 'msg' => 'This input is ' . number_format((int)$timelineRoute['input_char_count']) . " characters, so Timeline is using {$label} for reliability.", ]); } $validFocus = ['all', 'deadlines', 'hearings', 'cps']; $focus = in_array((string)($input['focus'] ?? ''), $validFocus, true) ? (string)$input['focus'] : 'all'; $confidenceFilter = (string)($input['confidence_filter'] ?? '') === 'high_medium' ? 'high_medium' : 'all'; $includeRelative = ($input['include_relative'] ?? true) !== false; $includeBackground = ($input['include_background'] ?? true) !== false; $userNotes = dbnToolsString($input, 'user_notes', 2000, false); $result = (new DbnLegalToolsService())->timeline( $text, $language, $engine, $focus, $confidenceFilter, $includeRelative, $includeBackground, $userNotes, fn(string $msg) => sseEmit('status', ['msg' => $msg]) ); $latency = (int)round((microtime(true) - $start) * 1000); if ($ftUid > 0) { $balance = dbnToolsFreeTierDeductAmount($ftUid, 'timeline', (int)$timelineRoute['credits'], [ 'requested_engine' => $timelineRoute['requested_engine'], 'effective_engine' => $timelineRoute['effective_engine'], 'auto_upgraded_engine' => $timelineRoute['auto_upgraded_engine'], 'input_char_count' => $timelineRoute['input_char_count'], ]); $result['balance'] = $balance; if (!headers_sent()) { header('X-Credits-Remaining: ' . $balance); } } $result['ok'] = true; $result['latency_ms'] = $latency; $result['trace_metadata'] = array_merge($result['trace_metadata'] ?? [], [ 'requested_engine' => $timelineRoute['requested_engine'], 'effective_engine' => $timelineRoute['effective_engine'], 'auto_upgraded_engine' => $timelineRoute['auto_upgraded_engine'], 'input_char_count' => $timelineRoute['input_char_count'], 'engine_limit_chars' => $timelineRoute['engine_limit_chars'], 'credits_charged' => $timelineRoute['credits'], ]); dbnToolsLogMetadata([ 'tool' => 'timeline', 'language' => $language, 'ok' => true, 'latency_ms' => $latency, 'chunk_count' => (int)($result['trace_metadata']['chunk_count'] ?? 0), 'source_count' => (int)($result['trace_metadata']['source_count'] ?? 0), 'deployment' => $result['trace_metadata']['deployment'] ?? null, ]); sseEmit('result', $result); } catch (DbnToolsHttpException $e) { $latency = (int)round((microtime(true) - $start) * 1000); dbnToolsLogMetadata([ 'tool' => 'timeline', 'language' => $language, 'ok' => false, 'latency_ms' => $latency, 'error_code' => $e->errorCode, ]); sseEmit('error', ['code' => $e->errorCode, 'message' => $e->getMessage()]); } catch (Throwable $e) { $latency = (int)round((microtime(true) - $start) * 1000); dbnToolsLogMetadata([ 'tool' => 'timeline', 'language' => $language, 'ok' => false, 'latency_ms' => $latency, 'error_code' => 'internal_error', ]); error_log('timeline-stream error: ' . $e->getMessage()); sseEmit('error', ['code' => 'internal_error', 'message' => 'The tool could not complete this request.']); }