$requestedEngine, 'plus' => $requestedEngine === 'azure_full' ? 'azure_mini' : $requestedEngine, default => in_array($requestedEngine, ['nova_lite', 'regex'], true) ? $requestedEngine : 'nova_lite', }; } public static function timelineRoute(int $userId, string $requestedEngine, string $text): array { $valid = ['nova_lite', 'azure_mini', 'azure_full']; $requestedEngine = in_array($requestedEngine, $valid, true) ? $requestedEngine : 'azure_mini'; $tierEngine = self::engineForUser($userId, $requestedEngine); $charCount = mb_strlen($text, 'UTF-8'); if ($charCount > self::TIMELINE_DEEP_MAX_CHARS) { throw new DbnToolsHttpException( 'This timeline input is too large after selected documents or My Case context were added. Split the file or use fewer selected documents.', 413, 'timeline_input_too_large', ['input_char_count' => $charCount, 'max_chars' => self::TIMELINE_DEEP_MAX_CHARS] ); } $effectiveEngine = $tierEngine; if ($charCount > self::timelineEngineMaxChars($effectiveEngine)) { $effectiveEngine = $charCount <= self::TIMELINE_STANDARD_MAX_CHARS ? 'azure_mini' : 'azure_full'; } elseif ($charCount > self::TIMELINE_STANDARD_CHAR_LIMIT && $effectiveEngine === 'nova_lite') { $effectiveEngine = $charCount <= self::TIMELINE_QUICK_MAX_CHARS ? 'nova_lite' : 'azure_mini'; } if ($charCount > self::timelineEngineMaxChars($effectiveEngine)) { $effectiveEngine = 'azure_full'; } $credits = self::timelineCreditsForSize($effectiveEngine, $charCount); $baseCredits = self::timelineAdvertisedCredits($requestedEngine); $requiresConfirmation = $credits > $baseCredits || self::timelineEngineRank($effectiveEngine) > self::timelineEngineRank($requestedEngine); $chunked = $charCount > self::timelineEngineLimit($effectiveEngine); return [ 'requested_engine' => $requestedEngine, 'tier_engine' => $tierEngine, 'effective_engine' => $effectiveEngine, 'auto_upgraded_engine' => $effectiveEngine !== $tierEngine, 'input_char_count' => $charCount, 'engine_limit_chars' => self::timelineEngineLimit($effectiveEngine), 'max_char_limit' => self::timelineEngineMaxChars($effectiveEngine), 'chunked_timeline' => $chunked, 'timeline_chunk_count' => $chunked ? (int)ceil($charCount / self::timelineChunkSize($effectiveEngine)) : 1, 'estimated_credits' => $credits, 'credits' => $credits, 'base_credits' => $baseCredits, 'requires_confirmation' => $requiresConfirmation, ]; } public static function assertTimelineQuoteAccepted(array $route, array $input): void { if (empty($route['requires_confirmation'])) { return; } $accepted = !empty($input['accepted_timeline_quote']) && (int)($input['accepted_credits'] ?? 0) === (int)$route['credits'] && (string)($input['accepted_effective_engine'] ?? '') === (string)$route['effective_engine']; if ($accepted) { return; } $engineLabel = self::timelineEngineLabel((string)$route['effective_engine']); throw new DbnToolsHttpException( 'This timeline is larger than the selected engine can handle at the advertised price. Confirm the quoted engine and credits before running.', 409, 'timeline_quote_required', ['timeline_quote' => array_merge($route, [ 'effective_engine_label' => $engineLabel, 'message' => 'Timeline will use ' . $engineLabel . ' for ' . number_format((int)$route['input_char_count']) . ' characters across about ' . (int)$route['timeline_chunk_count'] . ' chunk(s), costing ' . (int)$route['credits'] . ' credit(s).', ])] ); } public static function timelineCredits(string $engine): int { return self::timelineAdvertisedCredits($engine); } public static function timelineEngineLimit(string $engine): int { return match ($engine) { 'nova_lite' => self::TIMELINE_QUICK_CHAR_LIMIT, 'azure_mini' => self::TIMELINE_STANDARD_CHAR_LIMIT, default => self::TIMELINE_DEEP_CHAR_LIMIT, }; } public static function timelineChunkSize(string $engine): int { return match ($engine) { 'nova_lite' => 10000, 'azure_mini' => 16000, default => 30000, }; } public static function timelineEngineMaxChars(string $engine): int { return match ($engine) { 'nova_lite' => self::TIMELINE_QUICK_MAX_CHARS, 'azure_mini' => self::TIMELINE_STANDARD_MAX_CHARS, default => self::TIMELINE_DEEP_MAX_CHARS, }; } public static function timelineCreditsForSize(string $engine, int $charCount): int { return match ($engine) { 'nova_lite' => $charCount <= self::TIMELINE_QUICK_CHAR_LIMIT ? 1 : 2, 'azure_mini' => $charCount <= self::TIMELINE_STANDARD_CHAR_LIMIT ? 1 : ($charCount <= 180000 ? 2 : 3), default => $charCount <= self::TIMELINE_DEEP_CHAR_LIMIT ? 2 : ($charCount <= 350000 ? 4 : 6), }; } public static function timelineAdvertisedCredits(string $engine): int { return $engine === 'azure_full' ? 2 : 1; } public static function timelineEngineLabel(string $engine): string { return match ($engine) { 'nova_lite' => 'Quick', 'azure_full' => 'Deep', default => 'Standard', }; } private static function timelineEngineRank(string $engine): int { return match ($engine) { 'nova_lite' => 1, 'azure_mini' => 2, 'azure_full' => 3, default => 0, }; } }