diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 99e18b1..f945d8a 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -26,9 +26,8 @@ jobs: IYZICO_API_KEY=${{ secrets.IYZICO_API_KEY }} IYZICO_SECRET_KEY=${{ secrets.IYZICO_SECRET_KEY }} IYZICO_BASE_URL=${{ secrets.IYZICO_BASE_URL }} - AI_SAGLAYICI=${{ secrets.AI_SAGLAYICI }} - ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY }} - GEMINI_API_KEY=${{ secrets.GEMINI_API_KEY }} + OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY }} + OPENROUTER_MODEL=${{ secrets.OPENROUTER_MODEL }} EOF # BuildKit'te "docker build --memory" YOK SAYILIR (moby/buildkit#1362); diff --git a/data/app.db b/data/app.db index 14ae88f..8401bcc 100644 Binary files a/data/app.db and b/data/app.db differ diff --git a/next.config.ts b/next.config.ts index b73792d..b875547 100644 --- a/next.config.ts +++ b/next.config.ts @@ -12,15 +12,14 @@ const nextConfig: NextConfig = { output: "standalone", // Server-only paketler bundle dışında tutulur: Turbopack bunları derlemez // (build belleği ciddi düşer), runtime'da native require ile yüklenir ve - // standalone trace'i imaja kopyalar. @google/genai tek başına protobufjs + - // google-auth-library + ws zincirini sürüklüyordu (c2aba25 sonrası VPS'te - // build OOM'unun tetiği buydu). + // standalone trace'i imaja kopyalar. (Tarihçe: @google/genai'nin sürüklediği + // bağımlılık zinciri VPS'te build OOM'unu tetiklemişti; AI katmanı artık + // yalnızca openai paketiyle OpenRouter'a gidiyor.) serverExternalPackages: [ "better-sqlite3", "iyzipay", "@libsql/client", - "@google/genai", - "@anthropic-ai/sdk", + "openai", "resend", ], ...(kaynakKisitli diff --git a/package.json b/package.json index 6f5e230..4196465 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,7 @@ "tadimlik": "tsx scripts/tadimlik-uret.ts" }, "dependencies": { - "@anthropic-ai/sdk": "^0.112.4", "@cds/city": "^1.1.0", - "@google/genai": "^2.15.0", "@libsql/client": "^0.17.4", "@number-flow/react": "^0.6.2", "better-auth": "^1.6.23", @@ -33,6 +31,7 @@ "motion": "^12.42.2", "next": "16.2.10", "next-themes": "^0.4.6", + "openai": "^7.4.0", "radix-ui": "^1.6.3", "react": "19.2.4", "react-dom": "19.2.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10b8f8c..fc186a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,15 +8,9 @@ importers: .: dependencies: - '@anthropic-ai/sdk': - specifier: ^0.112.4 - version: 0.112.4(zod@4.4.3) '@cds/city': specifier: ^1.1.0 version: 1.1.0 - '@google/genai': - specifier: ^2.15.0 - version: 2.15.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)) '@libsql/client': specifier: ^0.17.4 version: 0.17.4 @@ -62,6 +56,9 @@ importers: next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + openai: + specifier: ^7.4.0 + version: 7.4.0(ws@8.21.1)(zod@4.4.3) radix-ui: specifier: ^1.6.3 version: 1.6.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -160,15 +157,6 @@ packages: '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} - '@anthropic-ai/sdk@0.112.4': - resolution: {integrity: sha512-7eXJJnrmBI5GMC6drrCiSkycVsT7crRZX3qv5HusLSm+qiILjmtqP7gf+UiT7ASu/7Gdj+Zfl4f2haV8wATKUg==} - hasBin: true - peerDependencies: - zod: ^3.25.0 || ^4.0.0 - peerDependenciesMeta: - zod: - optional: true - '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -286,10 +274,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.29.7': - resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} - engines: {node: '>=6.9.0'} - '@babel/template@7.29.7': resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} @@ -917,15 +901,6 @@ packages: '@floating-ui/utils@0.2.12': resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} - '@google/genai@2.15.0': - resolution: {integrity: sha512-Q41TvqwBQ9NcmWdh6qxY5qrpg+0FaVHD7febQoH007pykxzco4ohScBUP4BBBy+Q8j5D8euIBSRIBDfWuNVCKA==} - engines: {node: '>=20.0.0'} - peerDependencies: - '@modelcontextprotocol/sdk': ^1.25.2 - peerDependenciesMeta: - '@modelcontextprotocol/sdk': - optional: true - '@hono/node-server@1.19.14': resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} engines: {node: '>=18.14.1'} @@ -1289,33 +1264,6 @@ packages: '@postman/tunnel-agent@0.6.8': resolution: {integrity: sha512-2U42SmZW5G+suEcS++zB94sBWNO4qD4bvETGFRFDTqSpYl5ksfjcPqzYpgQgXgUmb6dfz+fAGbkcRamounGm0w==} - '@protobufjs/aspromise@1.1.2': - resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} - - '@protobufjs/base64@1.1.2': - resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} - - '@protobufjs/codegen@2.0.5': - resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} - - '@protobufjs/eventemitter@1.1.1': - resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} - - '@protobufjs/fetch@1.1.1': - resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} - - '@protobufjs/float@1.0.2': - resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} - - '@protobufjs/path@1.1.2': - resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} - - '@protobufjs/pool@1.1.0': - resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} - - '@protobufjs/utf8@1.1.2': - resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==} - '@radix-ui/number@1.1.2': resolution: {integrity: sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==} @@ -2287,9 +2235,6 @@ packages: '@types/react@19.2.17': resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} - '@types/retry@0.12.0': - resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} - '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -2717,9 +2662,6 @@ packages: resolution: {integrity: sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==} engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x || 26.x} - bignumber.js@9.3.1: - resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} - bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} @@ -2749,9 +2691,6 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - buffer-equal-constant-time@1.0.1: - resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} - buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -3092,10 +3031,6 @@ packages: resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} engines: {node: '>=0.10'} - data-uri-to-buffer@4.0.1: - resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} - engines: {node: '>= 12'} - data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -3336,9 +3271,6 @@ packages: ecc-jsbn@0.1.2: resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} - ecdsa-sig-formatter@1.0.11: - resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} - ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -3649,10 +3581,6 @@ packages: picomatch: optional: true - fetch-blob@3.2.0: - resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} - engines: {node: ^12.20 || >= 14.13} - figures@6.1.0: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} @@ -3694,10 +3622,6 @@ packages: forever-agent@0.6.1: resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} - formdata-polyfill@4.0.10: - resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} - engines: {node: '>=12.20.0'} - forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -3745,14 +3669,6 @@ packages: fuzzysort@3.1.0: resolution: {integrity: sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==} - gaxios@7.3.0: - resolution: {integrity: sha512-RB5vLV+vvQeoFPCX4QMK6/hjVkbIamPp1QSUD0CiZcnj12qbpiL+pLbYtgD+oZkWl0tl9z+o2Utp+MpM3QRhBA==} - engines: {node: '>=18'} - - gcp-metadata@8.1.2: - resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} - engines: {node: '>=18'} - generator-function@2.0.1: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} @@ -3822,14 +3738,6 @@ packages: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} - google-auth-library@10.9.1: - resolution: {integrity: sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw==} - engines: {node: '>=18'} - - google-logging-utils@1.1.3: - resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} - engines: {node: '>=14'} - gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -3900,10 +3808,6 @@ packages: resolution: {integrity: sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==} engines: {node: '>=0.10'} - https-proxy-agent@7.0.6: - resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} - engines: {node: '>= 14'} - human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -4206,19 +4110,12 @@ packages: engines: {node: '>=6'} hasBin: true - json-bigint@1.0.0: - resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} - json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - json-schema-to-ts@3.1.1: - resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} - engines: {node: '>=16'} - json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -4260,12 +4157,6 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} - jwa@2.0.1: - resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} - - jws@4.0.1: - resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} - katex@0.16.47: resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} hasBin: true @@ -4414,9 +4305,6 @@ packages: resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} engines: {node: '>=18'} - long@5.3.2: - resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} - longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -4729,19 +4617,10 @@ packages: resolution: {integrity: sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==} engines: {node: '>=10'} - node-domexception@1.0.0: - resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} - engines: {node: '>=10.5.0'} - deprecated: Use your platform's native DOMException instead - node-exports-info@1.6.2: resolution: {integrity: sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==} engines: {node: '>= 0.4'} - node-fetch@3.3.2: - resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - node-releases@2.0.51: resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} engines: {node: '>=18'} @@ -4825,6 +4704,27 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} + openai@7.4.0: + resolution: {integrity: sha512-+C9Muit5x8j9R8ej8ZzVgKcrVDtqFqTy9gxFdov0EItLgU68zrJtF9ZeT0cyqJQW9S3PCJkdFgADtRGquRBtew==} + engines: {node: '>=22.0.0'} + peerDependencies: + '@aws-sdk/credential-provider-node': '>=3.972.0 <4' + '@smithy/hash-node': '>=4.3.0 <5' + '@smithy/signature-v4': '>=5.4.0 <6' + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@aws-sdk/credential-provider-node': + optional: true + '@smithy/hash-node': + optional: true + '@smithy/signature-v4': + optional: true + ws: + optional: true + zod: + optional: true + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -4853,10 +4753,6 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - p-retry@4.6.2: - resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} - engines: {node: '>=8'} - p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -4990,10 +4886,6 @@ packages: property-information@7.2.0: resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} - protobufjs@7.6.5: - resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} - engines: {node: '>=12.0.0'} - proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -5176,10 +5068,6 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} - retry@0.13.1: - resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} - engines: {node: '>= 4'} - reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -5517,9 +5405,6 @@ packages: trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} - ts-algebra@2.0.0: - resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} - ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} @@ -5720,10 +5605,6 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - web-streams-polyfill@3.3.3: - resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} - engines: {node: '>= 8'} - which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -5817,13 +5698,6 @@ snapshots: package-manager-detector: 1.8.0 tinyexec: 1.3.0 - '@anthropic-ai/sdk@0.112.4(zod@4.4.3)': - dependencies: - json-schema-to-ts: 3.1.1 - standardwebhooks: 1.0.0 - optionalDependencies: - zod: 4.4.3 - '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -5987,8 +5861,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/runtime@7.29.7': {} - '@babel/template@7.29.7': dependencies: '@babel/code-frame': 7.29.7 @@ -6410,19 +6282,6 @@ snapshots: '@floating-ui/utils@0.2.12': {} - '@google/genai@2.15.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))': - dependencies: - google-auth-library: 10.9.1 - p-retry: 4.6.2 - protobufjs: 7.6.5 - ws: 8.21.1 - optionalDependencies: - '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - '@hono/node-server@1.19.14(hono@4.12.31)': dependencies: hono: 4.12.31 @@ -6651,29 +6510,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': - dependencies: - '@hono/node-server': 1.19.14(hono@4.12.31) - ajv: 8.20.0 - ajv-formats: 3.0.1(ajv@8.20.0) - content-type: 1.0.5 - cors: 2.8.6 - cross-spawn: 7.0.6 - eventsource: 3.0.7 - eventsource-parser: 3.1.0 - express: 5.2.1 - express-rate-limit: 8.6.0(express@5.2.1) - hono: 4.12.31 - jose: 6.2.3 - json-schema-typed: 8.0.2 - pkce-challenge: 5.0.1 - raw-body: 3.0.2 - zod: 4.4.3 - zod-to-json-schema: 3.25.2(zod@4.4.3) - transitivePeerDependencies: - - supports-color - optional: true - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 @@ -6757,26 +6593,6 @@ snapshots: dependencies: safe-buffer: 5.2.1 - '@protobufjs/aspromise@1.1.2': {} - - '@protobufjs/base64@1.1.2': {} - - '@protobufjs/codegen@2.0.5': {} - - '@protobufjs/eventemitter@1.1.1': {} - - '@protobufjs/fetch@1.1.1': - dependencies: - '@protobufjs/aspromise': 1.1.2 - - '@protobufjs/float@1.0.2': {} - - '@protobufjs/path@1.1.2': {} - - '@protobufjs/pool@1.1.0': {} - - '@protobufjs/utf8@1.1.2': {} - '@radix-ui/number@1.1.2': {} '@radix-ui/primitive@1.1.6': {} @@ -7819,8 +7635,6 @@ snapshots: dependencies: csstype: 3.2.3 - '@types/retry@0.12.0': {} - '@types/trusted-types@2.0.7': optional: true @@ -8210,8 +8024,6 @@ snapshots: bindings: 1.5.0 prebuild-install: 7.1.3 - bignumber.js@9.3.1: {} - bindings@1.5.0: dependencies: file-uri-to-path: 1.0.0 @@ -8259,8 +8071,6 @@ snapshots: node-releases: 2.0.51 update-browserslist-db: 1.2.3(browserslist@4.28.6) - buffer-equal-constant-time@1.0.1: {} - buffer-from@1.1.2: {} buffer@5.7.1: @@ -8603,8 +8413,6 @@ snapshots: dependencies: assert-plus: 1.0.0 - data-uri-to-buffer@4.0.1: {} - data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -8739,10 +8547,6 @@ snapshots: jsbn: 0.1.1 safer-buffer: 2.1.2 - ecdsa-sig-formatter@1.0.11: - dependencies: - safe-buffer: 5.2.1 - ee-first@1.1.1: {} electron-to-chromium@1.5.394: {} @@ -9304,11 +9108,6 @@ snapshots: optionalDependencies: picomatch: 4.0.5 - fetch-blob@3.2.0: - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 3.3.3 - figures@6.1.0: dependencies: is-unicode-supported: 2.1.0 @@ -9356,10 +9155,6 @@ snapshots: forever-agent@0.6.1: {} - formdata-polyfill@4.0.10: - dependencies: - fetch-blob: 3.2.0 - forwarded@0.2.0: {} framer-motion@12.42.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4): @@ -9402,22 +9197,6 @@ snapshots: fuzzysort@3.1.0: {} - gaxios@7.3.0: - dependencies: - extend: 3.0.2 - https-proxy-agent: 7.0.6 - node-fetch: 3.3.2 - transitivePeerDependencies: - - supports-color - - gcp-metadata@8.1.2: - dependencies: - gaxios: 7.3.0 - google-logging-utils: 1.1.3 - json-bigint: 1.0.0 - transitivePeerDependencies: - - supports-color - generator-function@2.0.1: {} gensync@1.0.0-beta.2: {} @@ -9486,19 +9265,6 @@ snapshots: define-properties: 1.2.1 gopd: 1.2.0 - google-auth-library@10.9.1: - dependencies: - base64-js: 1.5.1 - ecdsa-sig-formatter: 1.0.11 - gaxios: 7.3.0 - gcp-metadata: 8.1.2 - google-logging-utils: 1.1.3 - jws: 4.0.1 - transitivePeerDependencies: - - supports-color - - google-logging-utils@1.1.3: {} - gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -9591,13 +9357,6 @@ snapshots: jsprim: 2.0.2 sshpk: 1.18.0 - https-proxy-agent@7.0.6: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - human-signals@2.1.0: {} human-signals@8.0.1: {} @@ -9853,19 +9612,10 @@ snapshots: jsesc@3.1.0: {} - json-bigint@1.0.0: - dependencies: - bignumber.js: 9.3.1 - json-buffer@3.0.1: {} json-parse-even-better-errors@2.3.1: {} - json-schema-to-ts@3.1.1: - dependencies: - '@babel/runtime': 7.29.7 - ts-algebra: 2.0.0 - json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -9906,17 +9656,6 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 - jwa@2.0.1: - dependencies: - buffer-equal-constant-time: 1.0.1 - ecdsa-sig-formatter: 1.0.11 - safe-buffer: 5.2.1 - - jws@4.0.1: - dependencies: - jwa: 2.0.1 - safe-buffer: 5.2.1 - katex@0.16.47: dependencies: commander: 8.3.0 @@ -10036,8 +9775,6 @@ snapshots: chalk: 5.6.2 is-unicode-supported: 1.3.0 - long@5.3.2: {} - longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -10543,8 +10280,6 @@ snapshots: dependencies: semver: 7.8.5 - node-domexception@1.0.0: {} - node-exports-info@1.6.2: dependencies: array.prototype.flatmap: 1.3.3 @@ -10552,12 +10287,6 @@ snapshots: object.entries: 1.1.9 semver: 6.3.1 - node-fetch@3.3.2: - dependencies: - data-uri-to-buffer: 4.0.1 - fetch-blob: 3.2.0 - formdata-polyfill: 4.0.10 - node-releases@2.0.51: {} npm-run-path@4.0.1: @@ -10658,6 +10387,11 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 + openai@7.4.0(ws@8.21.1)(zod@4.4.3): + optionalDependencies: + ws: 8.21.1 + zod: 4.4.3 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -10701,11 +10435,6 @@ snapshots: dependencies: p-limit: 3.1.0 - p-retry@4.6.2: - dependencies: - '@types/retry': 0.12.0 - retry: 0.13.1 - p-try@2.2.0: {} package-manager-detector@1.8.0: {} @@ -10854,20 +10583,6 @@ snapshots: property-information@7.2.0: {} - protobufjs@7.6.5: - dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/base64': 1.1.2 - '@protobufjs/codegen': 2.0.5 - '@protobufjs/eventemitter': 1.1.1 - '@protobufjs/fetch': 1.1.1 - '@protobufjs/float': 1.0.2 - '@protobufjs/path': 1.1.2 - '@protobufjs/pool': 1.1.0 - '@protobufjs/utf8': 1.1.2 - '@types/node': 20.19.43 - long: 5.3.2 - proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -11147,8 +10862,6 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 - retry@0.13.1: {} - reusify@1.1.0: {} robust-predicates@3.0.3: {} @@ -11609,8 +11322,6 @@ snapshots: trough@2.2.0: {} - ts-algebra@2.0.0: {} - ts-api-utils@2.5.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -11860,8 +11571,6 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - web-streams-polyfill@3.3.3: {} - which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -11936,11 +11645,6 @@ snapshots: dependencies: zod: 3.25.76 - zod-to-json-schema@3.25.2(zod@4.4.3): - dependencies: - zod: 4.4.3 - optional: true - zod-validation-error@4.0.2(zod@4.4.3): dependencies: zod: 4.4.3 diff --git a/scripts/standalone-linkleri-onar.sh b/scripts/standalone-linkleri-onar.sh index f6ac605..81c2366 100644 --- a/scripts/standalone-linkleri-onar.sh +++ b/scripts/standalone-linkleri-onar.sh @@ -1,8 +1,8 @@ #!/bin/sh # Next standalone çıktısında eksik kök node_modules linklerini tamamlar. # -# Neden: Turbopack, serverExternalPackages'taki ESM paketleri (@google/genai, -# @anthropic-ai/sdk, resend) çalışma anında bare import ile yükler ama nft +# Neden: Turbopack, serverExternalPackages'taki ESM paketleri (openai, +# resend) çalışma anında bare import ile yükler ama nft # trace'ine yalnızca .pnpm gerçek yollarını yazar — kök symlink standalone'a # kopyalanmaz ve import MODULE_NOT_FOUND ile düşer. (CJS external'larda — # better-sqlite3, iyzipay — symlink trace'e giriyor, sorun yalnızca ESM'de.) diff --git a/scripts/tadimlik-uret.ts b/scripts/tadimlik-uret.ts index f9d2ec0..b3737e6 100644 --- a/scripts/tadimlik-uret.ts +++ b/scripts/tadimlik-uret.ts @@ -10,8 +10,8 @@ * pnpm tadimlik --bekleme=13000 # LLM çağrıları arası bekleme (ms) * * AI anahtarı yoksa (dev) mock metin üretir — akış uçtan uca test edilebilir. - * Gemini ücretsiz katmanı dakikada ~5 istek kabul eder; gerçek üretimde - * --bekleme=13000 gibi bir tempo kullan. + * OpenRouter'da katı bir dakikalık limit yok; --bekleme varsayılanı (500 ms) + * genelde yeterli, 429 görürsen tempoyu artır. * Anonim trafik LLM'e asla istek atmaz; tek maliyet bu script'in koşusudur. */ import { randomUUID } from "node:crypto"; @@ -128,8 +128,6 @@ async function metinUret( .filter(Boolean) .join("\n\n"); - // Not: Gemini'de thinking bütçesi de bu sınırdan düşer; dar tutulursa - // çıktı kırpılıp şema doğrulaması boşa düşüyor. return yapilandirilmisUret({ system: SISTEM, kullanici, diff --git a/src/app/listem/page.tsx b/src/app/listem/page.tsx index 58a5acb..681ade9 100644 --- a/src/app/listem/page.tsx +++ b/src/app/listem/page.tsx @@ -15,10 +15,12 @@ import { ListemGovde } from "./listem-govde"; import { ListemBosCta } from "./listem-bos-cta"; import { ListeUretici } from "./liste-uretici"; -// listeRevize aksiyonu 2 AI denemesi yapabiliyor (deneme başına 50 sn abort -// sınırı, bkz. lib/ai/cagri.ts); platform limiti AI zaman aşımından SONRA -// dolmalı ki kredi iadesi her zaman çalışabilsin. -export const maxDuration = 120; +// listeRevize aksiyonu 2 AI denemesi yapabiliyor (deneme başına 60 sn abort +// sınırı, bkz. lib/ai/cagri.ts) → en kötü ~120 sn. Platform limiti bundan +// SONRA dolmalı ki kredi iadesi her zaman çalışabilsin. Self-hosted docker'da +// bu değer etkisiz ama invaryant belgeli kalsın; öndeki reverse proxy'nin +// istek süresi limiti de >= 200 sn olmalı. +export const maxDuration = 300; export const metadata: Metadata = { title: "Listem", diff --git a/src/app/listem/sohbet-client.tsx b/src/app/listem/sohbet-client.tsx index 1865b96..536c42a 100644 --- a/src/app/listem/sohbet-client.tsx +++ b/src/app/listem/sohbet-client.tsx @@ -121,6 +121,7 @@ export function SohbetClient({ { id: asistanId, role: "assistant", content: "" }, ]); + let birikmis = ""; try { const res = await fetch("/api/soru", { method: "POST", @@ -151,7 +152,6 @@ export function SohbetClient({ }); const reader = res.body!.getReader(); const decoder = new TextDecoder(); - let birikmis = ""; for (;;) { const { done, value } = await reader.read(); if (done) break; @@ -165,7 +165,26 @@ export function SohbetClient({ // harcamasını görmez; bakiye orada da güncellensin. router.refresh(); } catch { - toast.error("Bağlantı koptu. Cevabı sayfayı yenileyerek görebilirsin."); + // Sunucu hata anında krediyi iade edip yarım cevabı kaydetmiş olabilir; + // tahmin etme: kayıtlı geçmişi ve bakiyeyi sunucudan çek (boş balon da + // bu senkronla temizlenir). Senkron da düşerse boş balonu elle kaldır. + try { + const res = await fetch("/api/soru"); + const data = res.ok ? await res.json() : null; + if (data?.mesajlar) setMesajlar(data.mesajlar); + if (typeof data?.kredi === "number") setKredi(data.kredi); + } catch {} + if (!birikmis) { + setMesajlar((m) => m.filter((x) => x.id !== asistanId)); + // Cevap hiç gelmedi; kullanıcının yazdığı soru kaybolmasın + setGirdi(mesaj); + } + router.refresh(); + toast.error( + birikmis + ? "Bağlantı koptu — cevabın buraya kadar üretilen kısmı kaydedildi." + : "Bağlantı koptu, cevap alınamadı. Kredin harcandıysa otomatik iade edildi.", + ); } finally { setBekliyor(false); } diff --git a/src/app/meraklisina/huni-semasi.tsx b/src/app/meraklisina/huni-semasi.tsx index 31c3f94..2c70cf9 100644 --- a/src/app/meraklisina/huni-semasi.tsx +++ b/src/app/meraklisina/huni-semasi.tsx @@ -18,8 +18,9 @@ flowchart TD H --> S{"Havuzda 24'ten az program mı kaldı?"} S -- "evet" --> G("Filtreler adım adım gevşetilir
önce il, sonra alan — listede açıkça belirtilir") G --> H - S -- "hayır" --> Y("Yapay zekâ havuzdan tam 24 tercih seçer
havuzda olmayan bölümü yazamaz, her satıra gerekçe + risk notu ekler") - Y --> L("24 tercihlik liste") + S -- "hayır" --> Y("Yapay zekâ havuzdan tam 24 tercih seçer
havuzda olmayan bölümü yazamaz, her satıra gerekçe yazar") + Y --> R("Risk notu ve trend özeti
gerçek sıralama geçmişinden hesaplanır, yapay zekâ yorumu değildir") + R --> L("24 tercihlik liste") L --> L1("üstte 4–6 hayal") L --> L2("ortada 12–14 dengeli") L --> L3("sonda 5–7 garanti") @@ -39,7 +40,7 @@ export function HuniSemasi() { return ( Her adım havuzu biraz daha daraltır; yapay zekâ listeyi bu havuzun{" "} diff --git a/src/app/sonuc/actions.ts b/src/app/sonuc/actions.ts index 025be4a..f978cc3 100644 --- a/src/app/sonuc/actions.ts +++ b/src/app/sonuc/actions.ts @@ -43,8 +43,15 @@ function siraTurGecerli(sira: number, tur: string): tur is PuanTuruKey { function aiHataMesaji(err: unknown): string { if (err instanceof RaporUretimHatasi) return err.message; // AbortSignal.timeout: DOMException "TimeoutError"; SDK'lar abort'u kendi - // hata sınıflarıyla sarabiliyor (ör. APIUserAbortError) — ada göre yakala. - if (err instanceof Error && /abort|timeout/i.test(err.name)) { + // hata sınıflarıyla sarabiliyor (ör. APIUserAbortError) ve openai/anthropic + // bu sınıflarda .name'i "Error" bırakıyor — ada ek olarak sınıf adına ve + // mesaja da bak ("Request was aborted." / "Request timed out."). + if ( + err instanceof Error && + /abort|timeout|timed out/i.test( + `${err.name} ${err.constructor.name} ${err.message}`, + ) + ) { return "Liste üretimi bu sefer çok uzun sürdü ve durduruldu; harcanan kredin otomatik iade edildi. Tekrar dener misin?"; } if (err instanceof Error && err.message === "AI_KEY_MISSING") { diff --git a/src/lib/ai/cagri.ts b/src/lib/ai/cagri.ts index 1187ab5..6fb0eff 100644 --- a/src/lib/ai/cagri.ts +++ b/src/lib/ai/cagri.ts @@ -1,43 +1,40 @@ import { z } from "zod"; -import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod"; -import { ThinkingLevel } from "@google/genai"; -import { - getAnthropic, - getGemini, - raporModeli, - saglayici, - sohbetModeli, -} from "./client"; +import { getOpenRouter, raporModeli, sohbetModeli } from "./client"; /** - * Sağlayıcıdan bağımsız AI çağrı katmanı. Anthropic ve Gemini'nin SDK'ları - * yapılandırılmış çıktı ve stream için farklı şekiller kullanıyor; rapor ve - * sohbet kodu bu farkı görmesin diye tek arayüz burada toplanıyor. + * AI çağrı katmanı: tüm istekler OpenRouter'ın OpenAI-uyumlu API'sine gider + * (tek kapı — kullanıcı kararı, 7 Ağu 2026). Rapor ve sohbet kodu SDK/istek + * ayrıntısını görmesin diye tek arayüz burada toplanıyor. */ /** - * Gemini JSON Schema'nın yalnızca bir alt kümesini destekliyor; zod'un ürettiği - * $schema ve additionalProperties alanları isteği reddettiriyor, ayıklanıyor. + * zod şemasını JSON Schema'ya çevirir: z.toJSONSchema'nın eklediği $schema + * alanı ayıklanır, her object düğümüne additionalProperties: false eklenir — + * OpenAI-uyumlu strict json_schema doğrulayıcıları bunu şart koşuyor. */ -function geminiSemasi(sema: z.ZodType): unknown { - const temizle = (dugum: unknown): unknown => { - if (Array.isArray(dugum)) return dugum.map(temizle); +function jsonSemasi(sema: z.ZodType): Record { + const donustur = (dugum: unknown): unknown => { + if (Array.isArray(dugum)) return dugum.map(donustur); if (dugum && typeof dugum === "object") { const cikti: Record = {}; for (const [k, v] of Object.entries(dugum as Record)) { - if (k === "$schema" || k === "additionalProperties") continue; - cikti[k] = temizle(v); + if (k === "$schema") continue; + cikti[k] = donustur(v); + } + if (cikti.type === "object" && cikti.additionalProperties === undefined) { + cikti.additionalProperties = false; } return cikti; } return dugum; }; - return temizle(z.toJSONSchema(sema)); + return donustur(z.toJSONSchema(sema)) as Record; } /** - * Gemini ücretsiz katmanı yoğunlukta 503/429 döndürüyor; bu geçici hatalarda - * artan bekleme ile yeniden dene. Kalıcı hatalar (400, 403) olduğu gibi fırlar. + * OpenRouter yoğunlukta/limitte 429, üst sağlayıcı arızasında 5xx dönebiliyor; + * bu geçici hatalarda artan bekleme ile yeniden dene. Kalıcı hatalar + * (400, 401, 403) olduğu gibi fırlar. */ const GECICI_KODLAR = new Set([429, 500, 502, 503, 504]); @@ -65,9 +62,10 @@ async function geciciHatadaTekrarla( * Tek yapılandırılmış üretim çağrısının üst süre sınırı. Süre, çağıranın * (server action) platform limitinden önce dolmalı ki hata iade kodunun * çalışabildiği pencerede fırlasın — fonksiyon dışarıdan öldürülürse - * harcanan kredi iade edilemiyor. + * harcanan kredi iade edilemiyor. Flash sınıfı model tipik ~10-20 sn'de + * biter; 60 sn üst sağlayıcı yavaşlamalarına pay bırakır. */ -const URETIM_ZAMAN_ASIMI_MS = 50_000; +const URETIM_ZAMAN_ASIMI_MS = 60_000; /** * Şemaya uyan tek seferlik çıktı üretir. Şemaya uymayan yanıtta null döner @@ -82,92 +80,78 @@ export async function yapilandirilmisUret(opts: { // Tek sinyal tüm iç denemeleri kapsar: süre dolunca bekleyen retry'lar da // anında düşer, toplam süre sınırın üstüne çıkamaz. const sinyal = AbortSignal.timeout(URETIM_ZAMAN_ASIMI_MS); - if (saglayici() === "gemini") { - const yanit = await geciciHatadaTekrarla(() => - getGemini().models.generateContent({ + const yanit = await geciciHatadaTekrarla(() => + getOpenRouter().chat.completions.create( + { model: raporModeli(), - contents: opts.kullanici, - config: { - abortSignal: sinyal, - systemInstruction: opts.system, - maxOutputTokens: opts.maxTokens, - responseMimeType: "application/json", - responseJsonSchema: geminiSemasi(opts.sema), - thinkingConfig: { thinkingLevel: ThinkingLevel.HIGH }, + max_tokens: opts.maxTokens, + // Görev küçük (havuzdan seçim + tek cümlelik gerekce) ve şemaya + // uymayan çıktıyı doğrulama döngüsü zaten yakalıyor; hibrit + // modellerde düşünme bütçe/süre yemesin. + reasoning_effort: "none", + messages: [ + { role: "system", content: opts.system }, + { role: "user", content: opts.kullanici }, + ], + response_format: { + type: "json_schema", + json_schema: { + name: "yanit", + strict: true, + schema: jsonSemasi(opts.sema), + }, }, - }), - ); - const metin = yanit.text; - if (!metin) return null; - try { - const parsed = opts.sema.safeParse(JSON.parse(metin)); - return parsed.success ? parsed.data : null; - } catch { - return null; - } - } - - const yanit = await getAnthropic().messages.parse( - { - model: raporModeli(), - max_tokens: opts.maxTokens, - thinking: { type: "adaptive" }, - system: opts.system, - messages: [{ role: "user", content: opts.kullanici }], - output_config: { format: zodOutputFormat(opts.sema) }, - }, - { signal: sinyal }, + }, + { signal: sinyal }, + ), ); - return yanit.parsed_output ?? null; + const metin = yanit.choices[0]?.message?.content; + if (!metin) return null; + try { + const parsed = opts.sema.safeParse(JSON.parse(metin)); + return parsed.success ? parsed.data : null; + } catch { + return null; + } } export type SohbetMesaji = { role: "user" | "assistant"; content: string }; /** - * Sohbet yanıtını parça parça üretir. Sağlayıcıya bakmaksızın düz metin - * parçaları yield eder; çağıran taraf hem stream'ler hem biriktirip kaydeder. + * Sohbet yanıtını parça parça üretir; düz metin parçaları yield eder, çağıran + * taraf hem stream'ler hem biriktirip kaydeder. Akış ortasında zaman aşımı + * olursa hata fırlar (route'taki kısmi kaydet + iade yolu devreye girer). */ export async function* sohbetAkisi(opts: { system: string; mesajlar: SohbetMesaji[]; maxTokens: number; }): AsyncGenerator { - if (saglayici() === "gemini") { - const akis = await geciciHatadaTekrarla(() => - getGemini().models.generateContentStream({ + // Asılı kalan üst sağlayıcıya karşı sigorta + const sinyal = AbortSignal.timeout(120_000); + const akis = await geciciHatadaTekrarla(() => + getOpenRouter().chat.completions.create( + { model: sohbetModeli(), - contents: opts.mesajlar.map((m) => ({ - // Gemini "assistant" yerine "model" rolünü kullanır - role: m.role === "assistant" ? "model" : "user", - parts: [{ text: m.content }], - })), - config: { - systemInstruction: opts.system, - maxOutputTokens: opts.maxTokens, - // Sohbet kısa ve hızlı olmalı; uzun düşünme adayı bekletiyor - thinkingConfig: { thinkingLevel: ThinkingLevel.LOW }, - }, - }), - ); - for await (const parca of akis) { - const metin = parca.text; - if (metin) yield metin; - } - return; - } - - const akis = getAnthropic().messages.stream({ - model: sohbetModeli(), - max_tokens: opts.maxTokens, - system: opts.system, - messages: opts.mesajlar, - }); - for await (const olay of akis) { - if ( - olay.type === "content_block_delta" && - olay.delta.type === "text_delta" - ) { - yield olay.delta.text; - } + max_tokens: opts.maxTokens, + // Sohbet 1024 token'a sığmalı; düşünme hem bütçeyi hem adayı bekletiyor + reasoning_effort: "none", + stream: true, + messages: [ + { role: "system", content: opts.system }, + ...opts.mesajlar, + ], + }, + { signal: sinyal }, + ), + ); + for await (const parca of akis) { + // Yalnızca delta.content akar; varsa delta.reasoning kullanıcıya sızmaz + const metin = parca.choices[0]?.delta?.content; + if (metin) yield metin; } + // openai SDK akış ortasındaki abort'u yutup iteratörü NORMAL bitiriyor + // (core/streaming: isAbortError -> return); kesik cevabın tam cevap + // sanılmaması ve iade yolunun çalışması için zaman aşımını elle fırlat. + if (sinyal.aborted) throw sinyal.reason; } diff --git a/src/lib/ai/client.ts b/src/lib/ai/client.ts index c147661..953ff9d 100644 --- a/src/lib/ai/client.ts +++ b/src/lib/ai/client.ts @@ -1,64 +1,52 @@ -import Anthropic from "@anthropic-ai/sdk"; -import { GoogleGenAI } from "@google/genai"; +import OpenAI from "openai"; -export type Saglayici = "anthropic" | "gemini"; - -/** AI_SAGLAYICI env'i ile seçilir; tanımsızsa hangi anahtar varsa o kullanılır. */ -export function saglayici(): Saglayici { - const secim = process.env.AI_SAGLAYICI?.trim().toLowerCase(); - if (secim === "gemini" || secim === "anthropic") return secim; - return process.env.GEMINI_API_KEY ? "gemini" : "anthropic"; -} - -/** Seçili sağlayıcının anahtarı tanımlı mı (mock moduna düşülecek mi)? */ -export function anahtarVar(): boolean { - return saglayici() === "gemini" - ? Boolean(process.env.GEMINI_API_KEY) - : Boolean(process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN); -} - -// Model seçimi (bilinçli): -// Anthropic — rapor tek seferlik ve yüksek değerli -> Opus 4.8; -// sohbet kredi başına marj hassas -> Haiku 4.5 (kullanıcı kararı, 22 Tem 2026). -// Gemini — ücretsiz katmanda yalnızca Flash ailesi açık (Pro'da kota 0), -// bu yüzden rapor ve sohbet aynı modeli kullanır; ayrım thinking seviyesinde. -const MODELLER: Record = { - anthropic: { rapor: "claude-opus-4-8", sohbet: "claude-haiku-4-5" }, - gemini: { rapor: "gemini-3.6-flash", sohbet: "gemini-3.6-flash" }, -}; +// Tüm AI akışı OpenRouter üzerinden tek kapıdan gider (kullanıcı kararı, +// 7 Ağu 2026 — önceki Anthropic/Gemini/lokal sağlayıcı seçimi kaldırıldı). +// Model .env'den değiştirilebilir; rapor ve sohbet istenirse ayrışabilir. +const VARSAYILAN_MODEL = "deepseek/deepseek-v4-flash"; export function raporModeli(): string { - return MODELLER[saglayici()].rapor; + return ( + process.env.OPENROUTER_RAPOR_MODEL?.trim() || + process.env.OPENROUTER_MODEL?.trim() || + VARSAYILAN_MODEL + ); } export function sohbetModeli(): string { - return MODELLER[saglayici()].sohbet; + return ( + process.env.OPENROUTER_SOHBET_MODEL?.trim() || + process.env.OPENROUTER_MODEL?.trim() || + VARSAYILAN_MODEL + ); } -const globalForAi = globalThis as unknown as { - __anthropic?: Anthropic; - __gemini?: GoogleGenAI; -}; - -export function getAnthropic(): Anthropic { - if (!process.env.ANTHROPIC_API_KEY && !process.env.ANTHROPIC_AUTH_TOKEN) { - throw new Error("AI_KEY_MISSING"); - } - if (!globalForAi.__anthropic) { - globalForAi.__anthropic = new Anthropic(); - } - return globalForAi.__anthropic; +/** API anahtarı tanımlı mı (mock moduna düşülecek mi)? */ +export function anahtarVar(): boolean { + return Boolean(process.env.OPENROUTER_API_KEY); } -export function getGemini(): GoogleGenAI { - const apiKey = process.env.GEMINI_API_KEY; +const globalForAi = globalThis as unknown as { __openrouter?: OpenAI }; + +export function getOpenRouter(): OpenAI { + const apiKey = process.env.OPENROUTER_API_KEY; if (!apiKey) { throw new Error("AI_KEY_MISSING"); } - if (!globalForAi.__gemini) { - globalForAi.__gemini = new GoogleGenAI({ apiKey }); + if (!globalForAi.__openrouter) { + globalForAi.__openrouter = new OpenAI({ + baseURL: "https://openrouter.ai/api/v1", + apiKey, + // Tek retry otoritesi cagri.ts'teki geciciHatadaTekrarla + maxRetries: 0, + // OpenRouter panelindeki kullanım dökümünde uygulama atfı (opsiyonel) + defaultHeaders: { + "HTTP-Referer": "https://kolaytercih.com", + "X-Title": "KolayTercih", + }, + }); } - return globalForAi.__gemini; + return globalForAi.__openrouter; } export const SORUMLULUK_REDDI = diff --git a/src/lib/ai/rapor.ts b/src/lib/ai/rapor.ts index 3fd5afa..2b10134 100644 --- a/src/lib/ai/rapor.ts +++ b/src/lib/ai/rapor.ts @@ -26,6 +26,24 @@ const RaporSchema = z.object({ uyarilar: z.array(z.string()).min(2).max(3), }); +// Yapay zekâdan istenen küçük şema (kullanıcı kararı, 6 Ağu 2026): riskNotu +// ve trendOzeti gerçek sıra geçmişinden deterministik üretilir; 5/13/6'lık +// dilim iskeleti ve tercih sırası da koddan gelir. Dilim başına SABİT uzunluklu +// ayrı listeler istenir ki dağılım şema düzeyinde (grammar/JSON-schema ile) +// zorlansın — reasoning kapalıyken model serbest dağılımı tutturamıyor +// (ölçüldü: 13 hayal / 0 garanti dönmüştü). AI yalnızca seçer + gerekce yazar. +const AiTercihSchema = z.object({ + programId: z.string(), + gerekce: z.string(), +}); +const AiRaporSchema = z.object({ + hayal: z.array(AiTercihSchema).length(5), + dengeli: z.array(AiTercihSchema).length(13), + garanti: z.array(AiTercihSchema).length(6), + genelDegerlendirme: z.string(), + uyarilar: z.array(z.string()).min(2).max(3), +}); + // uyarilar şemada zorunlu ama eski kayıtlı raporlarda yok → tipte opsiyonel export type RaporSonuc = Omit, "uyarilar"> & { uyarilar?: string[]; @@ -46,30 +64,77 @@ export type RaporSonuc = Omit, "uyarilar"> & { }; const SISTEM = `Sen KolayTercih'in kıdemli YKS tercih danışmanısın. Görevin: sana verilen -GERÇEK YÖK Atlas aday havuzundan, adayın sıralamasına göre 24 tercihlik dengeli -bir liste kurmak. +GERÇEK YÖK Atlas aday havuzundan adaya en uygun programları seçip üç listeyi +doldurmak: hayal (TAM 5 program), dengeli (TAM 13), garanti (TAM 6). KURALLAR (kesin): -- SADECE havuzdaki programId'leri kullan. Havuzda olmayan program ASLA önerme. -- Tam 24 tercih üret; sira alanı 1'den 24'e sıralı olsun. -- Liste yapısı: en üstte 4-6 hayal, ortada 12-14 dengeli, sonda 5-7 garanti. - Tercih sırası MUTLAKA iyi taban sıralamasından kötüye doğru gitmeli - (küçük taban sıralaması = daha zor bölüm üstte). -- METİN DİYETİ: aday 24 satır okuyacak; her alan TEK kısa cümle, dolgu söz yok. -- gerekce: bu programın bu aday için neden mantıklı olduğu (TEK cümle, - en fazla ~15 kelime, Türkçe). -- riskNotu: yerleşme ihtimali değerlendirmesi (TEK kısa cümle). ASLA "garanti", - "kesin", "yüzde yüz" deme; "yüksek/orta/düşük ihtimal" dili kullan. -- trendOzeti: son 4 yılın taban sıralaması hareketi, TEK cümle (ör. "2022'den - beri taban 45B'den 38B'ye indi, bölüm zorlaşıyor"). -- Kontenjan artışı/azalışı ve yerleşen sayısını değerlendirmene kat. +- SADECE havuzdaki programId'leri kullan; havuzda olmayan program ASLA önerme. + Aynı programId'yi birden fazla kez kullanma. +- Havuzdaki her programın "dilim" alanı var: hayal listesine yalnızca dilim + değeri "hayal" olan programları koy; dengeli ve garanti için de aynısı. +- Seçerken adayın ilgi alanlarını ve illerini, kontenjan/yerleşen hareketini + ve bölüm çeşitliliğini gözet. +- METİN DİYETİ: aday 24 satır okuyacak; gerekce TEK kısa cümle (en fazla ~15 + kelime, Türkçe): bu programın bu aday için neden mantıklı olduğu. Dolgu söz + yok; ASLA "garanti", "kesin", "yüzde yüz" deme. - Adayın notlarındaki şehir/bölüm tercihlerine saygı göster ama dengeyi bozma. - genelDegerlendirme: listenin genel stratejisi, EN FAZLA 2 cümle. Uyarıları buraya yazma. -- uyarilar: adaya 2-3 pratik, eyleme dönük uyarı; her biri tek kısa cümle.`; +- uyarilar: adaya 2-3 pratik, eyleme dönük uyarı; her biri tek kısa cümle. +(Tercih sırası, risk notu ve trend özeti koddan hesaplanır; sen yazmazsın.)`; export class RaporUretimHatasi extends Error {} +// --- Deterministik satır metinleri: risk ve trend gerçek YÖK Atlas sıra +// geçmişinden hesaplanır; model bu alanlarda jenerik dolgu yazıyordu, kod +// kesin rakam verir ("45B düzeyinden 38B düzeyine indi"). + +/** 45000 -> "45B", 1_200_000 -> "1,2M" (şemadaki öğrenci dili). */ +function siraKisa(n: number): string { + if (n >= 1_000_000) { + return `${(n / 1_000_000).toLocaleString("tr-TR", { maximumFractionDigits: 1 })}M`; + } + if (n >= 1000) return `${Math.round(n / 1000)}B`; + return String(n); +} + +function trendOzetiUret(p: AdayProgram): string { + const ham: Array<[number, number | null | undefined]> = [ + [2021, p.sira2021], + [2022, p.sira2022], + [2023, p.sira2023], + [2024, p.sira2024], + [2025, p.sira2025], + ]; + const seri = ham.filter((cift): cift is [number, number] => cift[1] != null); + if (seri.length < 2) { + return "Karşılaştırma için yeterli geçmiş yıl verisi yok; trendi temkinli oku."; + } + const [ilkYil, ilkSira] = seri[0]; + const [sonYil, sonSira] = seri[seri.length - 1]; + const oran = (sonSira - ilkSira) / ilkSira; + if (oran <= -0.1) { + return `${ilkYil}-${sonYil} arasında taban ${siraKisa(ilkSira)} düzeyinden ${siraKisa(sonSira)} düzeyine indi; bölüm zorlaşıyor.`; + } + if (oran >= 0.1) { + return `${ilkYil}-${sonYil} arasında taban ${siraKisa(ilkSira)} düzeyinden ${siraKisa(sonSira)} düzeyine çıktı; yerleşmek görece kolaylaşıyor.`; + } + return `${ilkYil}-${sonYil} arasında taban sıralaması ${siraKisa(sonSira)} düzeyinde stabil seyretti.`; +} + +function riskNotuUret(p: AdayProgram, adaySira: number): string { + switch (riskHesapla(p.sira2025 ?? p.sira2024, adaySira)) { + case "guvenli": + return "Taban sıralaması senin sıranın belirgin altında; yüksek ihtimalle yerleşirsin."; + case "az-riskli": + return "Taban sıralaması sırana yakın; orta ihtimalle yerleşilebilir."; + case "riskli": + return "Taban sıralaması senin sıranın üstünde kapandı; yerleşme ihtimali düşük."; + default: + return "Güncel taban sıralaması verisi yok; ihtimal öngörülemez, temkinli yaklaş."; + } +} + /** Havuz programını render için gereken bilgiye indirger. */ function programBilgisi(p: AdayProgram): RaporSonuc["programlar"][string] { const gecmis = [p.sira2021, p.sira2022, p.sira2023, p.sira2024, p.sira2025]; @@ -119,14 +184,9 @@ function mockRapor( sira: i + 1, dilim: p.dilim, gerekce: `(Dev mock) ${p.isim}, seçtiğin ilgi alanlarıyla ve ${params.sira.toLocaleString("tr-TR")}. sıralamanla uyumlu bir aday.`, - riskNotu: - p.dilim === "hayal" - ? "(Dev mock) Taban sıralaması senin üstünde kapandı; düşük ihtimal ama listenin başında yeri var." - : p.dilim === "dengeli" - ? "(Dev mock) Taban sıralaması senin civarında; orta-yüksek ihtimalle yerleşilebilir." - : "(Dev mock) Taban sıralaması senin belirgin altında; yüksek ihtimalle yerleşirsin.", - trendOzeti: - "(Dev mock) Son 4 yılda taban sıralaması görece stabil seyretti.", + // Risk ve trend mock'ta da gerçek veriden: canlı davranışla birebir aynı + riskNotu: riskNotuUret(p, params.sira), + trendOzeti: trendOzetiUret(p), })); const programlar: RaporSonuc["programlar"] = {}; @@ -205,9 +265,9 @@ export async function raporUret( .filter(Boolean) .join("\n\n"); - const gecerliIdler = new Set(havuz.map((p) => p.id)); + const havuzMap = new Map(havuz.map((p) => [p.id, p])); - let sonuc: z.infer | null = null; + let sonuc: z.infer | null = null; let sonHata = ""; for (let deneme = 0; deneme < 2; deneme++) { const parsed = await yapilandirilmisUret({ @@ -216,26 +276,40 @@ export async function raporUret( deneme === 0 ? kullaniciMesaji : `${kullaniciMesaji}\n\nÖNCEKİ DENEMENDE HATA VARDI: ${sonHata} Kurallara tam uy.`, - sema: RaporSchema, - maxTokens: 16000, + sema: AiRaporSchema, + // Küçük şema (24 satır × gerekce + genel + uyarılar) ~1-1.5k token; + // reasoning kapalıyken 4000 rahat bir tavan, kaçak uzun üretimi de keser. + maxTokens: 4000, }); if (!parsed) { - sonHata = "Çıktı şemaya uymadı."; + // Dilim uzunlukları (5/13/6) şemada sabit: sayı tutmazsa da buraya düşer + sonHata = "Çıktı şemaya uymadı (hayal 5, dengeli 13, garanti 6 olmalı)."; continue; } - // Halüsinasyon koruması: her programId havuzda olmalı + tam 24 öğe - const bilinmeyen = parsed.tercihler.filter( - (t) => !gecerliIdler.has(t.programId), + // Halüsinasyon koruması: id'ler havuzda, benzersiz ve doğru dilimde olmalı + const gruplar = [ + ["hayal", parsed.hayal], + ["dengeli", parsed.dengeli], + ["garanti", parsed.garanti], + ] as const; + const idler = gruplar.flatMap(([, liste]) => liste.map((t) => t.programId)); + const bilinmeyen = idler.filter((id) => !havuzMap.has(id)); + if (bilinmeyen.length > 0) { + sonHata = `Havuzda olmayan programId kullanıldı: ${bilinmeyen.slice(0, 3).join(", ")}.`; + continue; + } + if (new Set(idler).size !== idler.length) { + sonHata = "Aynı programId birden fazla kez kullanıldı; her program tek kez seçilmeli."; + continue; + } + const yanlisDilim = gruplar.flatMap(([dilim, liste]) => + liste + .filter((t) => havuzMap.get(t.programId)!.dilim !== dilim) + .map((t) => `${t.programId} (${dilim} listesinde ama havuzdaki dilimi farklı)`), ); - if (parsed.tercihler.length !== 24 || bilinmeyen.length > 0) { - sonHata = - parsed.tercihler.length !== 24 - ? `Liste ${parsed.tercihler.length} öğe içeriyor, tam 24 olmalı.` - : `Havuzda olmayan programId kullanıldı: ${bilinmeyen - .map((t) => t.programId) - .slice(0, 3) - .join(", ")}.`; + if (yanlisDilim.length > 0) { + sonHata = `Programlar yanlış dilim listesine konmuş: ${yanlisDilim.slice(0, 3).join(", ")}. Havuzdaki "dilim" alanına birebir uy.`; continue; } sonuc = parsed; @@ -252,7 +326,39 @@ export async function raporUret( for (const p of havuz) { programlar[p.id] = programBilgisi(p); } - return { ...sonuc, programlar }; + // Sıra ve satır metinleri deterministik: dilim içi iyi taban sıralamasından + // kötüye dizilir (hayal -> dengeli -> garanti zaten bu yönde akar), sira + // 1-24 koddan atanır; riskNotu + trendOzeti gerçek veriden üretilir. + const efektif = (id: string) => { + const p = havuzMap.get(id)!; + return p.sira2025 ?? p.sira2024 ?? Number.MAX_SAFE_INTEGER; + }; + const tercihler = ( + [ + ["hayal", sonuc.hayal], + ["dengeli", sonuc.dengeli], + ["garanti", sonuc.garanti], + ] as const + ).flatMap(([dilim, liste]) => + [...liste].sort((a, b) => efektif(a.programId) - efektif(b.programId)) + .map((t) => ({ dilim, ...t })), + ).map((t, i) => { + const p = havuzMap.get(t.programId)!; + return { + programId: t.programId, + sira: i + 1, + dilim: t.dilim, + gerekce: t.gerekce, + riskNotu: riskNotuUret(p, params.sira), + trendOzeti: trendOzetiUret(p), + }; + }); + return { + tercihler, + genelDegerlendirme: sonuc.genelDegerlendirme, + uyarilar: sonuc.uyarilar, + programlar, + }; } export function listeOzetiCikar(rapor: RaporSonuc, adaySira?: number): string { diff --git a/src/lib/rapor-havuzu.ts b/src/lib/rapor-havuzu.ts index 2845c3b..a4a01cb 100644 --- a/src/lib/rapor-havuzu.ts +++ b/src/lib/rapor-havuzu.ts @@ -34,6 +34,17 @@ export interface HavuzSonuc { const HEDEF = 24; const LIMIT = 60; +// Prompt'a giden nihai havuz: dilim başına aday sayısı. Arama LIMIT ile geniş +// yapılır (filtre/gevşetme zenginliği için), filtre sonrası buraya kırpılır — +// 20×3=60 program ≈ 8k token; 180 program 24k token tutuyor ve lokal/ücretsiz +// modellerde prefill + isabet maliyeti yüksek (ölçüm: 6 Ağu 2026). +const PROMPT_LIMIT_DILIM = 20; + +/** Dilim sırasını koruyarak her dilimden en fazla n aday bırakır. */ +function dilimBasinaKirp(havuz: AdayProgram[], n: number): AdayProgram[] { + const sayac: Record = { hayal: 0, dengeli: 0, garanti: 0 }; + return havuz.filter((p) => ++sayac[p.dilim] <= n); +} function dilimEkle(r: { hayal: Program[]; @@ -79,12 +90,18 @@ export function havuzOlustur(params: RaporParams): HavuzSonuc { ); const tamFiltreli = kategoriFiltre(tamHam); if (tamFiltreli.length >= HEDEF) { - return { havuz: tamFiltreli, gevsetildi: [] }; + return { + havuz: dilimBasinaKirp(tamFiltreli, PROMPT_LIMIT_DILIM), + gevsetildi: [], + }; } // 2. Kategori gevşet: aynı illerdeki kategori-dışı programlarla doldur if (kategoriler.length > 0 && tamHam.length >= HEDEF) { - return { havuz: tamHam, gevsetildi: ["kategori"] }; + return { + havuz: dilimBasinaKirp(tamHam, PROMPT_LIMIT_DILIM), + gevsetildi: ["kategori"], + }; } // 3. İl gevşet: il filtresini kaldırıp kategoriye geri dön @@ -98,12 +115,15 @@ export function havuzOlustur(params: RaporParams): HavuzSonuc { ); const ilsizFiltreli = kategoriFiltre(ilsizHam); if (ilsizFiltreli.length >= HEDEF) { - return { havuz: ilsizFiltreli, gevsetildi }; + return { + havuz: dilimBasinaKirp(ilsizFiltreli, PROMPT_LIMIT_DILIM), + gevsetildi, + }; } // 4. Hem il hem kategori gevşet: elde ne varsa if (kategoriler.length > 0) gevsetildi.push("kategori"); - return { havuz: ilsizHam, gevsetildi }; + return { havuz: dilimBasinaKirp(ilsizHam, PROMPT_LIMIT_DILIM), gevsetildi }; } /** Prompt'a enjekte edilecek kompakt satır (token tasarrufu). */