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). */