feat: OpenRouter entegrasyonu — tek anahtarla tüm modeller, latest alias'lar

- server/ai.mjs: OpenRouter öncelikli yönlendirme (routeFor). OPENROUTER_API_KEY
  tanımlıysa zincirdeki tüm modeller OpenRouter üzerinden çağrılır; doğrudan
  sağlayıcı anahtarı varsa o ezer
- Tüm modeller OpenRouter floating 'latest' alias'larına çevrildi (sabit sürüm
  yerine otomatik güncel kalır):
    ~z-ai/glm-latest · ~deepseek/deepseek-v4-flash-latest
    ~google/gemini-flash-latest · ~x-ai/grok-latest
    openai/gpt-chat-latest · ~anthropic/claude-opus-latest
- /api/ai/config artık her model için kullanılan yolu (openrouter|direct) döner
- AI Model Ayarları: canlı durum bandı (aktif modeller + yol) ve OpenRouter
  kurulum yönergesi; eski 'alias' alanı gerçek OpenRouter kimliğiyle değişti
- Analiz modali: tek OPENROUTER_API_KEY önerisi
This commit is contained in:
2026-08-28 13:37:40 +03:00
parent 7a396ddeb6
commit dd2bfa12e6
4 changed files with 137 additions and 54 deletions
+5 -5
View File
@@ -115,11 +115,11 @@ export const AiSpecAnalyzerModal = () => {
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4 flex gap-3">
<Info className="w-4 h-4 text-amber-700 shrink-0 mt-0.5" />
<p className="text-xs text-amber-900 leading-relaxed">
<strong>AI henüz yapılandırılmamış.</strong> Analizin çalışması için sunucuya en az bir sağlayıcı anahtarı ekleyin
(<code className="bg-amber-100 px-1 rounded">ZAI_API_KEY</code>, <code className="bg-amber-100 px-1 rounded">DEEPSEEK_API_KEY</code>,
{' '}<code className="bg-amber-100 px-1 rounded">GEMINI_API_KEY</code> …) ve yeniden dağıtın.
Model sırası, Admin → AI Model Ayarları'ndan belirlenir.
</p>
<strong>AI henüz yapılandırılmamış.</strong> En kolayı: sunucuya tek bir{' '}
<code className="bg-amber-100 px-1 rounded">OPENROUTER_API_KEY</code> ekleyin —
zincirdeki tüm modeller (GLM, DeepSeek, Gemini, Grok, GPT, Claude) bu tek anahtarla çalışır.
Dilerseniz tek tek sağlayıcı anahtarı da verebilirsiniz; doğrudan anahtar OpenRouter'ı ezer.
Model sırası Admin → AI Model Ayarları'ndan belirlenir.</p>
</div>
)}
+35 -3
View File
@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { useApp } from '../../context/AppContext';
import { Cpu, Check, ArrowUp, ArrowDown, X, Plus, Sparkles, Save, Shield } from 'lucide-react';
import { Cpu, Check, ArrowUp, ArrowDown, X, Plus, Sparkles, Save, Shield, Zap, AlertTriangle } from 'lucide-react';
import { getAiConfig } from '../../services/ai';
import confetti from 'canvas-confetti';
const RENK = {
@@ -17,6 +18,10 @@ const SIRA_ETIKET = ['Birincil (Primary)', '2. Yedek (Fallback)', '3. Yedek (Fal
export const AiModelSettings = () => {
const { AI_MODEL_CATALOG, aiModelConfig, setAiModelConfig, getModelById } = useApp();
const [saved, setSaved] = useState(false);
const [cfg, setCfg] = useState(null);
useEffect(() => { getAiConfig().then(setCfg); }, []);
const aktifYol = cfg?.providers?.[0]?.yol;
const chain = aiModelConfig.chain;
const available = AI_MODEL_CATALOG.filter(m => !chain.includes(m.id));
@@ -58,6 +63,30 @@ export const AiModelSettings = () => {
return (
<div className="space-y-6 animate-fade-in">
{/* Canlı AI durumu */}
{cfg && (
cfg.configured ? (
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-4 flex gap-3">
<Zap className="w-4 h-4 text-emerald-700 shrink-0 mt-0.5" />
<div className="text-xs text-emerald-900 leading-relaxed">
<strong>AI aktif</strong> — {cfg.providers.length} model kullanılabilir
{aktifYol === 'openrouter' ? ' (OpenRouter üzerinden tek anahtarla)' : ' (doğrudan sağlayıcı anahtarlarıyla)'}.
<div className="font-mono text-[11px] mt-1 text-emerald-800">
{cfg.providers.map((p) => `${p.label}: ${p.model}`).join(' · ')}
</div>
</div>
</div>
) : (
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4 flex gap-3">
<AlertTriangle className="w-4 h-4 text-amber-700 shrink-0 mt-0.5" />
<p className="text-xs text-amber-900 leading-relaxed">
<strong>AI anahtarı yok.</strong> Sunucuya tek bir <code className="bg-amber-100 px-1 rounded">OPENROUTER_API_KEY</code> ekleyin —
aşağıdaki tüm modeller bu tek anahtarla çalışır. Aşağıdaki sıralama yalnızca tercih kaydeder;
çağrılar anahtar eklendiğinde başlar.
</p>
</div>
)
)}
{/* Header */}
<div className="bg-linear-to-r from-slate-900 via-indigo-950 to-blue-900 text-white p-6 rounded-2xl shadow-lg border border-indigo-800/40 flex items-center justify-between">
<div className="flex items-center gap-3">
@@ -117,7 +146,10 @@ export const AiModelSettings = () => {
</span>
)}
</div>
<div className="text-[11px] text-slate-500 font-mono mt-1">{m.alias} <span className="text-slate-400">→</span> <span className="text-slate-700 font-semibold">{m.model}</span></div>
<div className="text-[11px] text-slate-500 font-mono mt-1">
<span className="text-slate-700 font-semibold">{m.or}</span>
<span className="text-slate-400"> · doğrudan: </span>{m.model}
</div>
<div className="text-[10px] text-slate-400 mt-0.5">{SIRA_ETIKET[idx] || `${idx + 1}. sırada`}</div>
</div>