Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/ahlfs_buatjalan.json
T

1 line
36 KiB
JSON

{"repo": "ahlfs/buatjalan", "files": {"app/Services/AIService.php": {"81b9a0fefe501278275df99dbad7c2e65488596a": {"t": 1785746173.0977695, "n": 14971, "h": "68f6a7a72978401b", "c": "<?php\n\nnamespace App\\Services;\n\nuse Illuminate\\Support\\Facades\\Http;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Str;\n\nclass AIService\n{\n /**\n * Call the configured AI provider with a prompt.\n * Supports multiple API keys separated by commas.\n * Automatically falls back to the next key if one fails/exhausts.\n *\n * @param string $prompt\n * @return array\n */\n public static function call(string $prompt): array\n {\n $provider = config('services.ai.provider', 'iyh');\n $providerConfig = config(\"services.ai.providers.{$provider}\");\n\n if (empty($providerConfig) || empty($providerConfig['key'])) {\n Log::error(\"API key is not configured for provider: {$provider} in .env file.\");\n return [\n 'error' => \"API Key untuk provider '\" . strtoupper($provider) . \"' belum diisi di berkas .env Anda.\"\n ];\n }\n\n $rawKeys = $providerConfig['key'];\n $model = $providerConfig['model'];\n\n // Split comma-separated keys and trim spaces\n $apiKeys = array_filter(array_map('trim', explode(',', $rawKeys)));\n\n if (empty($apiKeys)) {\n Log::error(\"No valid API keys found after parsing for provider: {$provider}\");\n return [\n 'error' => \"API Key untuk provider '\" . strtoupper($provider) . \"' kosong atau tidak valid.\"\n ];\n }\n\n $lastError = 'Unknown error';\n $totalKeys = count($apiKeys);\n\n foreach ($apiKeys as $index => $apiKey) {\n $keyIndexNum = $index + 1;\n Log::info(\"Attempting AI generation with {$provider} using API Key {$keyIndexNum} of {$totalKeys}...\");\n\n try {\n $rawJson = null;\n\n if ($provider === 'gemini') {\n $response = Http::timeout(120)->post(\n \"https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key=\" . $apiKey,\n [\n 'contents' => [\n [\n 'parts' => [\n ['text' => $prompt]\n ]\n ]\n ],\n 'generationConfig' => [\n 'responseMimeType' => 'application/json'\n ]\n ]\n );\n\n if ($response->successful()) {\n $body = $response->json();\n $rawJson = $body['candidates'][0]['content']['parts'][0]['text'] ?? '';\n } else {\n $lastError = $response->body() ?: $response->reason();\n Log::warning(\"Gemini API key index {$keyIndexNum} failed. Response: {$lastError}\");\n continue;\n }\n } elseif (in_array($provider, ['openai', 'xai', 'deepseek', 'openrouter', 'iyh', 'ninerouter'])) {\n $endpoints = [\n 'openai' => 'https://api.openai.com/v1/chat/completions',\n 'xai' => 'https://api.x.ai/v1/chat/completions',\n 'deepseek' => 'https://api.deepseek.com/chat/completions',\n 'openrouter' => 'https://openrouter.ai/api/v1/chat/completions',\n 'iyh' => 'https://api.iyh.app/v1/chat/completions',\n 'ninerouter' => rtrim($providerConfig['url'] ?? 'https://api.9router.com/v1', '/') . '/chat/completions',\n ];\n\n $request = Http::timeout(120)->withToken($apiKey);\n\n if ($provider === 'openrouter') {\n $request->withHeaders([\n 'HTTP-Referer' => config('app.url', 'http://localhost:8000'),\n 'X-Title' => config('app.name', 'BuatJalan'),\n ]);\n }\n\n $response = $request->post($endpoints[$provider], [\n 'model' => $model,\n 'messages' => [\n ['role' => 'user', 'content' => $prompt]\n ],\n 'response_format' => ['type' => 'json_object'],\n 'stream' => false,\n ]);\n\n if ($response->successful()) {\n $rawBody = $response->body();\n if (Str::startsWith(trim($rawBody), 'data:')) {\n $lines = explode(\"\\n\", $rawBody);\n $rawJson = '';\n foreach ($lines as $line) {\n $line = trim($line);\n if (Str::startsWith($line, 'data:')) {\n $dataText = trim(Str::after($line, 'data:'));\n if ($dataText === '[DONE]') {\n continue;\n }\n $chunk = json_decode($dataText, true);\n if (isset($chunk['choices'][0]['delta']['content'])) {\n $rawJson .= $chunk['choices'][0]['delta']['content'];\n }\n }\n }\n } else {\n $body = $response->json();\n $rawJson = $body['choices'][0]['message']['content'] ?? '';\n }\n } else {\n $lastError = $response->body() ?: $response->reason();\n Log::warning(strtoupper($provider) . \" API key index {$keyIndexNum} failed. Response: {$lastError}\");\n continue;\n }\n } elseif ($provider === 'anthropic') {\n $response = Http::timeout(120)\n ->withHeaders([\n 'x-api-key' => $apiKey,\n 'anthropic-version' => '2023-06-01',\n 'content-type' => 'application/json',\n ])\n ->post('https://api.anthropic.com/v1/messages', [\n 'model' => $model,\n 'max_tokens' => 4000,\n 'messages' => [\n ['role' => 'user', 'content' => $prompt]\n ]\n ]);\n\n if ($response->successful()) {\n $body = $response->json();\n $rawJson = $body['content'][0]['text'] ?? '';\n } else {\n $lastError = $response->body() ?: $response->reason();\n Log::warning(\"Anthropic API key index {$keyIndexNum} failed. Response: {$lastError}\");\n continue;\n }\n } else {\n return ['error' => \"Provider AI '\" . strtoupper($provider) . \"' tidak didukung.\"];\n }\n\n if ($rawJson !== null) {\n // Clean up code block ticks if AI accidentally wraps it\n $rawJson = trim($rawJson);\n\n // 1. Remove <think>...</think> tags (e.g. DeepSeek R1, Qwen3 thinking models)\n if (preg_match('/<think>.*?<\\/think>/is', $rawJson)) {\n $rawJson = preg_replace('/<think>.*?<\\/think>/is', '', $rawJson);\n $rawJson = trim($rawJson);\n }\n\n // 2. Strip markdown code block wrappers\n if (Str::startsWith($rawJson, '```json')) {\n $rawJson = Str::after($rawJson, '```json');\n } elseif (Str::startsWith($rawJson, '```')) {\n $rawJson = Str::after($rawJson, '```');\n }\n if (Str::endsWith($rawJson, '```')) {\n $rawJson = Str::beforeLast($rawJson, '```');\n }\n $rawJson = trim($rawJson);\n\n // 3. Extract first JSON object block (handles conversational prefix/suffix)\n if (!Str::startsWith($rawJson, '{')) {\n if (preg_match('/\\{.*\\}/s', $rawJson, $matches)) {\n $rawJson = $matches[0];\n }\n }\n\n // 4. Sanitize JSON (remove comments, trailing commas, escape literal control characters)\n $rawJson = static::sanitizeJson($rawJson);\n\n // 5. Attempt to repair truncated JSON (model cut off mid-response)\n // Count unmatched braces and close them\n $data = json_decode($rawJson, true);\n if (json_last_error() !== JSON_ERROR_NONE) {\n $repaired = static::repairTruncatedJson($rawJson);\n if ($repaired !== null) {\n $data = json_decode($repaired, true);\n if (json_last_error() === JSON_ERROR_NONE) {\n Log::warning(\"{$provider} response was truncated and auto-repaired.\");\n $rawJson = $repaired;\n }\n }\n }\n\n if (json_last_error() !== JSON_ERROR_NONE || empty($data['title'])) {\n $jsonErrorMsg = json_last_error_msg();\n $tailSnippet = mb_substr($rawJson, -300);\n Log::error(\"Failed to parse JSON response from {$provider} (key index {$keyIndexNum}). JSON error: {$jsonErrorMsg}. Tail of raw response: ...{$tailSnippet}\");\n $lastError = 'Gagal melakukan parsing JSON dari model AI. (' . $jsonErrorMsg . ')';\n continue;\n }\n\n Log::info(\"AI generation with {$provider} succeeded using API Key {$keyIndexNum} of {$totalKeys}.\");\n return ['success' => true, 'data' => $data];\n }\n\n } catch (\\Exception $e) {\n $lastError = $e->getMessage();\n Log::error(\"Exception occurred using {$provider} API key index {$keyIndexNum}: {$lastError}\");\n continue;\n }\n }\n\n // If we get here, all API keys failed\n Log::error(\"All {$totalKeys} API keys for provider {$provider} failed.\");\n return [\n 'error' => \"Semua API Key (\" . $totalKeys . \") untuk \" . strtoupper($provider) . \" gagal digunakan atau habis kuotanya. Error terakhir: {$lastError}\"\n ];\n }\n\n /**\n * Sanitize JSON string before decoding.\n * Removes JavaScript-style comments, trailing commas, and escapes literal control characters.\n *\n * @param string $json\n * @return string\n */\n protected static function sanitizeJson(string $json): string\n {\n // 1. Remove comments\n // Multi-line comments: /* ... */\n $json = preg_replace('!/\\*.*?\\*/!s', '', $json);\n // Single-line comments: // ..., but ignore double-slashes in URLs (http:// or https://)\n $json = preg_replace('!^[ \\t]*//.*$!m', '', $json); \n $json = preg_replace('!(?<!:|http:|https:)//.*$!m', '', $json); \n\n // 2. Remove trailing commas before closing braces/brackets\n $json = preg_replace('/,\\s*([\\]}])/m', '$1', $json);\n\n // 3. Fix unescaped literal control characters inside double quotes\n $len = strlen($json);\n $clean = '';\n $inString = false;\n $escape = false;\n\n for ($i = 0; $i < $len; $i++) {\n $char = $json[$i];\n\n if ($escape) {\n $clean .= $char;\n $escape = false;\n continue;\n }\n\n if ($char === '\\\\' && $inString) {\n $clean .= $char;\n $escape = true;\n continue;\n }\n\n if ($char === '\"') {\n $inString = !$inString;\n $clean .= $char;\n continue;\n }\n\n if ($inString) {\n if ($char === \"\\n\") {\n $clean .= '\\n';\n } elseif ($char === \"\\r\") {\n $clean .= '\\r';\n } elseif ($char === \"\\t\") {\n $clean .= '\\t';\n } else {\n $clean .= $char;\n }\n } else {\n $clean .= $char;\n }\n }\n\n return $clean;\n }\n\n /**\n * Attempt to repair a truncated JSON string by closing any unmatched braces/brackets.\n * This handles cases where a model's response is cut off before completion (e.g. token limit).\n *\n * @param string $json\n * @return string|null Repaired JSON string, or null if it cannot be repaired.\n */\n protected static function repairTruncatedJson(string $json): ?string\n {\n $stack = [];\n $inString = false;\n $escape = false;\n $lastGoodPos = 0;\n\n for ($i = 0, $len = strlen($json); $i < $len; $i++) {\n $char = $json[$i];\n\n if ($escape) {\n $escape = false;\n continue;\n }\n if ($char === '\\\\' && $inString) {\n $escape = true;\n continue;\n }\n if ($char === '\"') {\n $inString = !$inString;\n if (!$inString) {\n $lastGoodPos = $i;\n }\n continue;\n }\n if ($inString) {\n continue;\n }\n\n if ($char === '{' || $char === '[') {\n $stack[] = $char === '{' ? '}' : ']';\n } elseif ($char === '}' || $char === ']') {\n if (!empty($stack) && end($stack) === $char) {\n array_pop($stack);\n $lastGoodPos = $i;\n }\n } elseif (!in_array($char, [',', ':', ' ', \"\\n\", \"\\r\", \"\\t\"])) {\n $lastGoodPos = $i;\n }\n }\n\n if (empty($stack)) {\n // JSON is already balanced\n return $json;\n }\n\n // Trim trailing comma or whitespace before closing\n $truncated = rtrim(substr($json, 0, $lastGoodPos + 1), \", \\t\\n\\r\");\n\n // Close all open containers in reverse order\n $repaired = $truncated . implode('', array_reverse($stack));\n\n // Validate repaired result\n json_decode($repaired);\n return json_last_error() === JSON_ERROR_NONE ? $repaired : null;\n }\n}\n"}, "b7e117391a4357805fb03d5d6fa2470c2eb12b0d": {"t": 1785760315.6901503, "n": 12881, "h": "ffd4eb435b1299cb", "c": "<?php\n\nnamespace App\\Services;\n\nuse Illuminate\\Support\\Facades\\Http;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Str;\n\nclass AIService\n{\n /**\n * Call the configured AI provider with a prompt.\n * Supports multiple API keys separated by commas.\n * Automatically falls back to the next key if one fails/exhausts.\n *\n * @param string $prompt\n * @return array\n */\n public static function call(string $prompt): array\n {\n $provider = config('services.ai.provider', 'iyh');\n $providerConfig = config(\"services.ai.providers.{$provider}\");\n\n if (empty($providerConfig) || empty($providerConfig['key'])) {\n Log::error(\"API key is not configured for provider: {$provider} in .env file.\");\n return [\n 'error' => \"API Key untuk provider '\" . strtoupper($provider) . \"' belum diisi di berkas .env Anda.\"\n ];\n }\n\n $rawKeys = $providerConfig['key'];\n $model = $providerConfig['model'];\n\n // Split comma-separated keys and trim spaces\n $apiKeys = array_filter(array_map('trim', explode(',', $rawKeys)));\n\n if (empty($apiKeys)) {\n Log::error(\"No valid API keys found after parsing for provider: {$provider}\");\n return [\n 'error' => \"API Key untuk provider '\" . strtoupper($provider) . \"' kosong atau tidak valid.\"\n ];\n }\n\n $lastError = 'Unknown error';\n $totalKeys = count($apiKeys);\n\n foreach ($apiKeys as $index => $apiKey) {\n $keyIndexNum = $index + 1;\n Log::info(\"Attempting AI generation with {$provider} using API Key {$keyIndexNum} of {$totalKeys}...\");\n\n try {\n $rawJson = null;\n\n if ($provider === 'gemini') {\n $response = Http::timeout(120)->post(\n \"https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key=\" . $apiKey,\n [\n 'contents' => [\n [\n 'parts' => [\n ['text' => $prompt]\n ]\n ]\n ],\n 'generationConfig' => [\n 'responseMimeType' => 'application/json'\n ]\n ]\n );\n\n if ($response->successful()) {\n $body = $response->json();\n $rawJson = $body['candidates'][0]['content']['parts'][0]['text'] ?? '';\n } else {\n $lastError = $response->body() ?: $response->reason();\n Log::warning(\"Gemini API key index {$keyIndexNum} failed. Response: {$lastError}\");\n continue;\n }\n } elseif (in_array($provider, ['openai', 'xai', 'deepseek', 'openrouter', 'iyh', 'ninerouter'])) {\n $endpoints = [\n 'openai' => 'https://api.openai.com/v1/chat/completions',\n 'xai' => 'https://api.x.ai/v1/chat/completions',\n 'deepseek' => 'https://api.deepseek.com/chat/completions',\n 'openrouter' => 'https://openrouter.ai/api/v1/chat/completions',\n 'iyh' => 'https://api.iyh.app/v1/chat/completions',\n 'ninerouter' => rtrim($providerConfig['url'] ?? 'https://api.9router.com/v1', '/') . '/chat/completions',\n ];\n\n $request = Http::timeout(120)->withToken($apiKey);\n\n if ($provider === 'openrouter') {\n $request->withHeaders([\n 'HTTP-Referer' => config('app.url', 'http://localhost:8000'),\n 'X-Title' => config('app.name', 'BuatJalan'),\n ]);\n }\n\n $response = $request->post($endpoints[$provider], [\n 'model' => $model,\n 'messages' => [\n ['role' => 'user', 'content' => $prompt]\n ],\n 'response_format' => ['type' => 'json_object'],\n 'stream' => false,\n ]);\n\n if ($response->successful()) {\n $rawBody = $response->body();\n if (Str::startsWith(trim($rawBody), 'data:')) {\n $lines = explode(\"\\n\", $rawBody);\n $rawJson = '';\n foreach ($lines as $line) {\n $line = trim($line);\n if (Str::startsWith($line, 'data:')) {\n $dataText = trim(Str::after($line, 'data:'));\n if ($dataText === '[DONE]') {\n continue;\n }\n $chunk = json_decode($dataText, true);\n if (isset($chunk['choices'][0]['delta']['content'])) {\n $rawJson .= $chunk['choices'][0]['delta']['content'];\n }\n }\n }\n } else {\n $body = $response->json();\n $rawJson = $body['choices'][0]['message']['content'] ?? '';\n }\n } else {\n $lastError = $response->body() ?: $response->reason();\n Log::warning(strtoupper($provider) . \" API key index {$keyIndexNum} failed. Response: {$lastError}\");\n continue;\n }\n } elseif ($provider === 'anthropic') {\n $response = Http::timeout(120)\n ->withHeaders([\n 'x-api-key' => $apiKey,\n 'anthropic-version' => '2023-06-01',\n 'content-type' => 'application/json',\n ])\n ->post('https://api.anthropic.com/v1/messages', [\n 'model' => $model,\n 'max_tokens' => 4000,\n 'messages' => [\n ['role' => 'user', 'content' => $prompt]\n ]\n ]);\n\n if ($response->successful()) {\n $body = $response->json();\n $rawJson = $body['content'][0]['text'] ?? '';\n } else {\n $lastError = $response->body() ?: $response->reason();\n Log::warning(\"Anthropic API key index {$keyIndexNum} failed. Response: {$lastError}\");\n continue;\n }\n } else {\n return ['error' => \"Provider AI '\" . strtoupper($provider) . \"' tidak didukung.\"];\n }\n\n if ($rawJson !== null) {\n // Clean up code block ticks if AI accidentally wraps it\n $rawJson = trim($rawJson);\n\n // 1. Remove <think>...</think> tags (e.g. DeepSeek R1, Qwen3 thinking models)\n if (preg_match('/<think>.*?<\\/think>/is', $rawJson)) {\n $rawJson = preg_replace('/<think>.*?<\\/think>/is', '', $rawJson);\n $rawJson = trim($rawJson);\n }\n\n // 2. Strip markdown code block wrappers\n if (Str::startsWith($rawJson, '```json')) {\n $rawJson = Str::after($rawJson, '```json');\n } elseif (Str::startsWith($rawJson, '```')) {\n $rawJson = Str::after($rawJson, '```');\n }\n if (Str::endsWith($rawJson, '```')) {\n $rawJson = Str::beforeLast($rawJson, '```');\n }\n $rawJson = trim($rawJson);\n\n // 3. Extract first JSON object block (handles conversational prefix/suffix)\n if (!Str::startsWith($rawJson, '{')) {\n if (preg_match('/\\{.*\\}/s', $rawJson, $matches)) {\n $rawJson = $matches[0];\n }\n }\n\n // 4. Attempt to repair truncated JSON (model cut off mid-response)\n // Count unmatched braces and close them\n $data = json_decode($rawJson, true);\n if (json_last_error() !== JSON_ERROR_NONE) {\n $repaired = static::repairTruncatedJson($rawJson);\n if ($repaired !== null) {\n $data = json_decode($repaired, true);\n if (json_last_error() === JSON_ERROR_NONE) {\n Log::warning(\"{$provider} response was truncated and auto-repaired.\");\n $rawJson = $repaired;\n }\n }\n }\n\n if (json_last_error() !== JSON_ERROR_NONE || empty($data['title'])) {\n $jsonErrorMsg = json_last_error_msg();\n $tailSnippet = mb_substr($rawJson, -300);\n Log::error(\"Failed to parse JSON response from {$provider} (key index {$keyIndexNum}). JSON error: {$jsonErrorMsg}. Tail of raw response: ...{$tailSnippet}\");\n $lastError = 'Gagal melakukan parsing JSON dari model AI. (' . $jsonErrorMsg . ')';\n continue;\n }\n\n Log::info(\"AI generation with {$provider} succeeded using API Key {$keyIndexNum} of {$totalKeys}.\");\n return ['success' => true, 'data' => $data];\n }\n\n } catch (\\Exception $e) {\n $lastError = $e->getMessage();\n Log::error(\"Exception occurred using {$provider} API key index {$keyIndexNum}: {$lastError}\");\n continue;\n }\n }\n\n // If we get here, all API keys failed\n Log::error(\"All {$totalKeys} API keys for provider {$provider} failed.\");\n return [\n 'error' => \"Semua API Key (\" . $totalKeys . \") untuk \" . strtoupper($provider) . \" gagal digunakan atau habis kuotanya. Error terakhir: {$lastError}\"\n ];\n }\n\n /**\n * Attempt to repair a truncated JSON string by closing any unmatched braces/brackets.\n * This handles cases where a model's response is cut off before completion (e.g. token limit).\n *\n * @param string $json\n * @return string|null Repaired JSON string, or null if it cannot be repaired.\n */\n protected static function repairTruncatedJson(string $json): ?string\n {\n $stack = [];\n $inString = false;\n $escape = false;\n $lastGoodPos = 0;\n\n for ($i = 0, $len = strlen($json); $i < $len; $i++) {\n $char = $json[$i];\n\n if ($escape) {\n $escape = false;\n continue;\n }\n if ($char === '\\\\' && $inString) {\n $escape = true;\n continue;\n }\n if ($char === '\"') {\n $inString = !$inString;\n if (!$inString) {\n $lastGoodPos = $i;\n }\n continue;\n }\n if ($inString) {\n continue;\n }\n\n if ($char === '{' || $char === '[') {\n $stack[] = $char === '{' ? '}' : ']';\n } elseif ($char === '}' || $char === ']') {\n if (!empty($stack) && end($stack) === $char) {\n array_pop($stack);\n $lastGoodPos = $i;\n }\n } elseif (!in_array($char, [',', ':', ' ', \"\\n\", \"\\r\", \"\\t\"])) {\n $lastGoodPos = $i;\n }\n }\n\n if (empty($stack)) {\n // JSON is already balanced\n return $json;\n }\n\n // Trim trailing comma or whitespace before closing\n $truncated = rtrim(substr($json, 0, $lastGoodPos + 1), \", \\t\\n\\r\");\n\n // Close all open containers in reverse order\n $repaired = $truncated . implode('', array_reverse($stack));\n\n // Validate repaired result\n json_decode($repaired);\n return json_last_error() === JSON_ERROR_NONE ? $repaired : null;\n }\n}\n"}, "6dbe68b0754cc0a093de24385f7c3ee4f21c6ce0": {"t": 1785760317.4721556, "n": 7770, "h": "ca0d5fce15983379", "c": "<?php\n\nnamespace App\\Services;\n\nuse Illuminate\\Support\\Facades\\Http;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Str;\n\nclass AIService\n{\n /**\n * Call the configured AI provider with a prompt.\n * Supports multiple API keys separated by commas.\n * Automatically falls back to the next key if one fails/exhausts.\n *\n * @param string $prompt\n * @return array\n */\n public static function call(string $prompt): array\n {\n $provider = config('services.ai.provider', 'iyh');\n $providerConfig = config(\"services.ai.providers.{$provider}\");\n\n if (empty($providerConfig) || empty($providerConfig['key'])) {\n Log::error(\"API key is not configured for provider: {$provider} in .env file.\");\n return [\n 'error' => \"API Key untuk provider '\" . strtoupper($provider) . \"' belum diisi di berkas .env Anda.\"\n ];\n }\n\n $rawKeys = $providerConfig['key'];\n $model = $providerConfig['model'];\n\n // Split comma-separated keys and trim spaces\n $apiKeys = array_filter(array_map('trim', explode(',', $rawKeys)));\n\n if (empty($apiKeys)) {\n Log::error(\"No valid API keys found after parsing for provider: {$provider}\");\n return [\n 'error' => \"API Key untuk provider '\" . strtoupper($provider) . \"' kosong atau tidak valid.\"\n ];\n }\n\n $lastError = 'Unknown error';\n $totalKeys = count($apiKeys);\n\n foreach ($apiKeys as $index => $apiKey) {\n $keyIndexNum = $index + 1;\n Log::info(\"Attempting AI generation with {$provider} using API Key {$keyIndexNum} of {$totalKeys}...\");\n\n try {\n $rawJson = null;\n\n if ($provider === 'gemini') {\n $response = Http::timeout(120)->post(\n \"https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key=\" . $apiKey,\n [\n 'contents' => [\n [\n 'parts' => [\n ['text' => $prompt]\n ]\n ]\n ],\n 'generationConfig' => [\n 'responseMimeType' => 'application/json'\n ]\n ]\n );\n\n if ($response->successful()) {\n $body = $response->json();\n $rawJson = $body['candidates'][0]['content']['parts'][0]['text'] ?? '';\n } else {\n $lastError = $response->body() ?: $response->reason();\n Log::warning(\"Gemini API key index {$keyIndexNum} failed. Response: {$lastError}\");\n continue;\n }\n } elseif (in_array($provider, ['openai', 'xai', 'deepseek', 'openrouter', 'iyh'])) {\n $endpoints = [\n 'openai' => 'https://api.openai.com/v1/chat/completions',\n 'xai' => 'https://api.x.ai/v1/chat/completions',\n 'deepseek' => 'https://api.deepseek.com/chat/completions',\n 'openrouter' => 'https://openrouter.ai/api/v1/chat/completions',\n 'iyh' => 'https://api.iyh.app/v1/chat/completions',\n ];\n\n $request = Http::timeout(120)->withToken($apiKey);\n\n if ($provider === 'openrouter') {\n $request->withHeaders([\n 'HTTP-Referer' => config('app.url', 'http://localhost:8000'),\n 'X-Title' => config('app.name', 'BuatJalan'),\n ]);\n }\n\n $response = $request->post($endpoints[$provider], [\n 'model' => $model,\n 'messages' => [\n ['role' => 'user', 'content' => $prompt]\n ],\n 'response_format' => ['type' => 'json_object']\n ]);\n\n if ($response->successful()) {\n $body = $response->json();\n $rawJson = $body['choices'][0]['message']['content'] ?? '';\n } else {\n $lastError = $response->body() ?: $response->reason();\n Log::warning(strtoupper($provider) . \" API key index {$keyIndexNum} failed. Response: {$lastError}\");\n continue;\n }\n } elseif ($provider === 'anthropic') {\n $response = Http::timeout(120)\n ->withHeaders([\n 'x-api-key' => $apiKey,\n 'anthropic-version' => '2023-06-01',\n 'content-type' => 'application/json',\n ])\n ->post('https://api.anthropic.com/v1/messages', [\n 'model' => $model,\n 'max_tokens' => 4000,\n 'messages' => [\n ['role' => 'user', 'content' => $prompt]\n ]\n ]);\n\n if ($response->successful()) {\n $body = $response->json();\n $rawJson = $body['content'][0]['text'] ?? '';\n } else {\n $lastError = $response->body() ?: $response->reason();\n Log::warning(\"Anthropic API key index {$keyIndexNum} failed. Response: {$lastError}\");\n continue;\n }\n } else {\n return ['error' => \"Provider AI '\" . strtoupper($provider) . \"' tidak didukung.\"];\n }\n\n if ($rawJson !== null) {\n // Clean up code block ticks if AI accidentally wraps it\n $rawJson = trim($rawJson);\n if (Str::startsWith($rawJson, '```json')) {\n $rawJson = Str::after($rawJson, '```json');\n }\n if (Str::endsWith($rawJson, '```')) {\n $rawJson = Str::beforeLast($rawJson, '```');\n }\n $rawJson = trim($rawJson);\n\n $data = json_decode($rawJson, true);\n\n if (json_last_error() !== JSON_ERROR_NONE || empty($data['title'])) {\n Log::error(\"Failed to parse JSON response from {$provider} (key index {$keyIndexNum}). Raw response: \" . $rawJson);\n $lastError = 'Gagal melakukan parsing JSON dari model AI.';\n continue;\n }\n\n Log::info(\"AI generation with {$provider} succeeded using API Key {$keyIndexNum} of {$totalKeys}.\");\n return ['success' => true, 'data' => $data];\n }\n\n } catch (\\Exception $e) {\n $lastError = $e->getMessage();\n Log::error(\"Exception occurred using {$provider} API key index {$keyIndexNum}: {$lastError}\");\n continue;\n }\n }\n\n // If we get here, all API keys failed\n Log::error(\"All {$totalKeys} API keys for provider {$provider} failed.\");\n return [\n 'error' => \"Semua API Key (\" . $totalKeys . \") untuk \" . strtoupper($provider) . \" gagal digunakan atau habis kuotanya. Error terakhir: {$lastError}\"\n ];\n }\n}\n"}}}}