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:
+91
-40
@@ -1,55 +1,79 @@
|
|||||||
/**
|
/**
|
||||||
* Gerçek AI şartname analizi — çoklu sağlayıcı + fallback zinciri.
|
* Gerçek AI şartname analizi — OpenRouter öncelikli, çoklu sağlayıcı fallback.
|
||||||
* Bağımlılık yok (Node 18+ built-in fetch).
|
* Bağımlılık yok (Node 18+ built-in fetch).
|
||||||
*
|
*
|
||||||
* Env (hangisi varsa o sağlayıcı kullanılabilir olur):
|
* EN KOLAY KURULUM — tek anahtar, tüm modeller:
|
||||||
* ZAI_API_KEY → GLM (Z.ai)
|
* OPENROUTER_API_KEY=sk-or-v1-...
|
||||||
* DEEPSEEK_API_KEY → DeepSeek
|
* Bu tanımlıysa zincirdeki tüm modeller OpenRouter üzerinden çağrılır.
|
||||||
* GEMINI_API_KEY → Gemini (Google)
|
|
||||||
* XAI_API_KEY → Grok (xAI)
|
|
||||||
* OPENAI_API_KEY → GPT (OpenAI)
|
|
||||||
* ANTHROPIC_API_KEY → Claude (Anthropic)
|
|
||||||
*
|
*
|
||||||
* Model kimlikleri env ile değiştirilebilir (ör. GLM_MODEL=glm-5.3).
|
* İsteğe bağlı doğrudan sağlayıcı anahtarları (OpenRouter'ı ezer):
|
||||||
|
* ZAI_API_KEY, DEEPSEEK_API_KEY, GEMINI_API_KEY,
|
||||||
|
* XAI_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY
|
||||||
|
*
|
||||||
|
* Model kimlikleri env ile değiştirilebilir (ör. GLM_MODEL, GLM_OR_MODEL).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
||||||
|
|
||||||
export const PROVIDERS = {
|
export const PROVIDERS = {
|
||||||
glm: {
|
glm: {
|
||||||
label: 'GLM', provider: 'Z.ai', envKey: 'ZAI_API_KEY',
|
label: 'GLM', provider: 'Z.ai', envKey: 'ZAI_API_KEY',
|
||||||
model: () => process.env.GLM_MODEL || 'glm-5.3',
|
model: () => process.env.GLM_MODEL || 'glm-5.3',
|
||||||
|
or: () => process.env.GLM_OR_MODEL || '~z-ai/glm-latest',
|
||||||
url: 'https://api.z.ai/api/paas/v4/chat/completions', style: 'openai',
|
url: 'https://api.z.ai/api/paas/v4/chat/completions', style: 'openai',
|
||||||
},
|
},
|
||||||
deepseek: {
|
deepseek: {
|
||||||
label: 'DeepSeek', provider: 'DeepSeek', envKey: 'DEEPSEEK_API_KEY',
|
label: 'DeepSeek', provider: 'DeepSeek', envKey: 'DEEPSEEK_API_KEY',
|
||||||
model: () => process.env.DEEPSEEK_MODEL || 'deepseek-chat',
|
model: () => process.env.DEEPSEEK_MODEL || 'deepseek-chat',
|
||||||
|
or: () => process.env.DEEPSEEK_OR_MODEL || '~deepseek/deepseek-v4-flash-latest',
|
||||||
url: 'https://api.deepseek.com/chat/completions', style: 'openai',
|
url: 'https://api.deepseek.com/chat/completions', style: 'openai',
|
||||||
},
|
},
|
||||||
gemini: {
|
gemini: {
|
||||||
label: 'Gemini', provider: 'Google', envKey: 'GEMINI_API_KEY',
|
label: 'Gemini', provider: 'Google', envKey: 'GEMINI_API_KEY',
|
||||||
model: () => process.env.GEMINI_MODEL || 'gemini-flash-latest',
|
model: () => process.env.GEMINI_MODEL || 'gemini-flash-latest',
|
||||||
|
or: () => process.env.GEMINI_OR_MODEL || '~google/gemini-flash-latest',
|
||||||
style: 'gemini',
|
style: 'gemini',
|
||||||
},
|
},
|
||||||
grok: {
|
grok: {
|
||||||
label: 'Grok', provider: 'xAI', envKey: 'XAI_API_KEY',
|
label: 'Grok', provider: 'xAI', envKey: 'XAI_API_KEY',
|
||||||
model: () => process.env.XAI_MODEL || 'grok-4',
|
model: () => process.env.XAI_MODEL || 'grok-4.6',
|
||||||
|
or: () => process.env.XAI_OR_MODEL || '~x-ai/grok-latest',
|
||||||
url: 'https://api.x.ai/v1/chat/completions', style: 'openai',
|
url: 'https://api.x.ai/v1/chat/completions', style: 'openai',
|
||||||
},
|
},
|
||||||
gpt: {
|
gpt: {
|
||||||
label: 'GPT', provider: 'OpenAI', envKey: 'OPENAI_API_KEY',
|
label: 'GPT', provider: 'OpenAI', envKey: 'OPENAI_API_KEY',
|
||||||
model: () => process.env.OPENAI_MODEL || 'gpt-5.2',
|
model: () => process.env.OPENAI_MODEL || 'gpt-5.6-sol',
|
||||||
|
or: () => process.env.OPENAI_OR_MODEL || 'openai/gpt-chat-latest',
|
||||||
url: 'https://api.openai.com/v1/chat/completions', style: 'openai',
|
url: 'https://api.openai.com/v1/chat/completions', style: 'openai',
|
||||||
},
|
},
|
||||||
claude: {
|
claude: {
|
||||||
label: 'Claude', provider: 'Anthropic', envKey: 'ANTHROPIC_API_KEY',
|
label: 'Claude', provider: 'Anthropic', envKey: 'ANTHROPIC_API_KEY',
|
||||||
model: () => process.env.ANTHROPIC_MODEL || 'claude-sonnet-5',
|
model: () => process.env.ANTHROPIC_MODEL || 'claude-opus-5',
|
||||||
|
or: () => process.env.ANTHROPIC_OR_MODEL || '~anthropic/claude-opus-latest',
|
||||||
url: 'https://api.anthropic.com/v1/messages', style: 'anthropic',
|
url: 'https://api.anthropic.com/v1/messages', style: 'anthropic',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const openRouterKey = () => process.env.OPENROUTER_API_KEY || null;
|
||||||
|
|
||||||
|
/** Bir model için hangi yol kullanılacak: doğrudan sağlayıcı mı, OpenRouter mı? */
|
||||||
|
export function routeFor(id) {
|
||||||
|
const p = PROVIDERS[id];
|
||||||
|
if (!p) return null;
|
||||||
|
if (process.env[p.envKey]) return { yol: 'direct', key: process.env[p.envKey], model: p.model() };
|
||||||
|
if (openRouterKey()) return { yol: 'openrouter', key: openRouterKey(), model: p.or() };
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export function availableProviders() {
|
export function availableProviders() {
|
||||||
return Object.entries(PROVIDERS)
|
return Object.keys(PROVIDERS)
|
||||||
.filter(([, p]) => Boolean(process.env[p.envKey]))
|
.map((id) => {
|
||||||
.map(([id, p]) => ({ id, label: p.label, provider: p.provider, model: p.model() }));
|
const r = routeFor(id);
|
||||||
|
if (!r) return null;
|
||||||
|
const p = PROVIDERS[id];
|
||||||
|
return { id, label: p.label, provider: p.provider, model: r.model, yol: r.yol };
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function aiConfigured() {
|
export function aiConfigured() {
|
||||||
@@ -93,12 +117,41 @@ function buildUserPrompt({ text, fileName, tender }) {
|
|||||||
return parts.join('\n');
|
return parts.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function callOpenAiStyle(p, key, userPrompt, signal) {
|
// --- OpenRouter (OpenAI uyumlu) ---
|
||||||
|
async function callOpenRouter(model, key, userPrompt, signal) {
|
||||||
|
const res = await fetch(OPENROUTER_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${key}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
// OpenRouter sıralamaları için önerilen başlıklar
|
||||||
|
'HTTP-Referer': process.env.APP_URL || 'https://ihaleist.seymata.com',
|
||||||
|
'X-Title': 'Ihaleist',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model,
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: SYSTEM_PROMPT },
|
||||||
|
{ role: 'user', content: userPrompt },
|
||||||
|
],
|
||||||
|
temperature: 0.2,
|
||||||
|
max_tokens: 4000,
|
||||||
|
response_format: { type: 'json_object' },
|
||||||
|
}),
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
if (!res.ok) throw new Error(data?.error?.message || `HTTP ${res.status}`);
|
||||||
|
if (data?.error) throw new Error(data.error.message || 'OpenRouter hatası');
|
||||||
|
return data?.choices?.[0]?.message?.content || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function callOpenAiStyle(p, key, model, userPrompt, signal) {
|
||||||
const res = await fetch(p.url, {
|
const res = await fetch(p.url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
|
headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: p.model(),
|
model,
|
||||||
messages: [
|
messages: [
|
||||||
{ role: 'system', content: SYSTEM_PROMPT },
|
{ role: 'system', content: SYSTEM_PROMPT },
|
||||||
{ role: 'user', content: userPrompt },
|
{ role: 'user', content: userPrompt },
|
||||||
@@ -113,8 +166,8 @@ async function callOpenAiStyle(p, key, userPrompt, signal) {
|
|||||||
return data?.choices?.[0]?.message?.content || '';
|
return data?.choices?.[0]?.message?.content || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function callGemini(p, key, userPrompt, signal) {
|
async function callGemini(p, key, model, userPrompt, signal) {
|
||||||
const url = `https://generativelanguage.googleapis.com/v1beta/models/${p.model()}:generateContent?key=${encodeURIComponent(key)}`;
|
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${encodeURIComponent(key)}`;
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -130,16 +183,12 @@ async function callGemini(p, key, userPrompt, signal) {
|
|||||||
return data?.candidates?.[0]?.content?.parts?.map((x) => x.text).join('') || '';
|
return data?.candidates?.[0]?.content?.parts?.map((x) => x.text).join('') || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function callAnthropic(p, key, userPrompt, signal) {
|
async function callAnthropic(p, key, model, userPrompt, signal) {
|
||||||
const res = await fetch(p.url, {
|
const res = await fetch(p.url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'x-api-key': key, 'anthropic-version': '2023-06-01', 'Content-Type': 'application/json' },
|
||||||
'x-api-key': key,
|
|
||||||
'anthropic-version': '2023-06-01',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: p.model(),
|
model,
|
||||||
max_tokens: 4000,
|
max_tokens: 4000,
|
||||||
system: SYSTEM_PROMPT,
|
system: SYSTEM_PROMPT,
|
||||||
messages: [{ role: 'user', content: userPrompt }],
|
messages: [{ role: 'user', content: userPrompt }],
|
||||||
@@ -154,7 +203,6 @@ async function callAnthropic(p, key, userPrompt, signal) {
|
|||||||
function extractJson(raw) {
|
function extractJson(raw) {
|
||||||
if (!raw) throw new Error('Model boş yanıt döndürdü');
|
if (!raw) throw new Error('Model boş yanıt döndürdü');
|
||||||
let s = String(raw).trim();
|
let s = String(raw).trim();
|
||||||
// ```json ... ``` sarmalını temizle
|
|
||||||
const fence = s.match(/```(?:json)?\s*([\s\S]*?)```/);
|
const fence = s.match(/```(?:json)?\s*([\s\S]*?)```/);
|
||||||
if (fence) s = fence[1].trim();
|
if (fence) s = fence[1].trim();
|
||||||
const start = s.indexOf('{');
|
const start = s.indexOf('{');
|
||||||
@@ -163,10 +211,7 @@ function extractJson(raw) {
|
|||||||
return JSON.parse(s.slice(start, end + 1));
|
return JSON.parse(s.slice(start, end + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Zinciri sırayla dener; ilk başarılı sonucu döndürür. */
|
||||||
* Zinciri sırayla dener; ilk başarılı sonucu döndürür.
|
|
||||||
* @returns {{result:object, model:object, attempts:Array}}
|
|
||||||
*/
|
|
||||||
export async function analyzeSpec({ text, fileName, tender, chain }) {
|
export async function analyzeSpec({ text, fileName, tender, chain }) {
|
||||||
const order = (Array.isArray(chain) && chain.length ? chain : ['glm', 'deepseek', 'gemini', 'grok', 'gpt', 'claude'])
|
const order = (Array.isArray(chain) && chain.length ? chain : ['glm', 'deepseek', 'gemini', 'grok', 'gpt', 'claude'])
|
||||||
.filter((id) => PROVIDERS[id]);
|
.filter((id) => PROVIDERS[id]);
|
||||||
@@ -175,27 +220,33 @@ export async function analyzeSpec({ text, fileName, tender, chain }) {
|
|||||||
|
|
||||||
for (const id of order) {
|
for (const id of order) {
|
||||||
const p = PROVIDERS[id];
|
const p = PROVIDERS[id];
|
||||||
const key = process.env[p.envKey];
|
const route = routeFor(id);
|
||||||
if (!key) {
|
if (!route) {
|
||||||
attempts.push({ id, ok: false, error: `${p.envKey} tanımlı değil` });
|
attempts.push({ id, ok: false, error: `anahtar yok (${p.envKey} veya OPENROUTER_API_KEY)` });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const ac = new AbortController();
|
const ac = new AbortController();
|
||||||
const timer = setTimeout(() => ac.abort(), 90000);
|
const timer = setTimeout(() => ac.abort(), 90000);
|
||||||
try {
|
try {
|
||||||
let raw;
|
let raw;
|
||||||
if (p.style === 'gemini') raw = await callGemini(p, key, userPrompt, ac.signal);
|
if (route.yol === 'openrouter') {
|
||||||
else if (p.style === 'anthropic') raw = await callAnthropic(p, key, userPrompt, ac.signal);
|
raw = await callOpenRouter(route.model, route.key, userPrompt, ac.signal);
|
||||||
else raw = await callOpenAiStyle(p, key, userPrompt, ac.signal);
|
} else if (p.style === 'gemini') {
|
||||||
|
raw = await callGemini(p, route.key, route.model, userPrompt, ac.signal);
|
||||||
|
} else if (p.style === 'anthropic') {
|
||||||
|
raw = await callAnthropic(p, route.key, route.model, userPrompt, ac.signal);
|
||||||
|
} else {
|
||||||
|
raw = await callOpenAiStyle(p, route.key, route.model, userPrompt, ac.signal);
|
||||||
|
}
|
||||||
const result = extractJson(raw);
|
const result = extractJson(raw);
|
||||||
attempts.push({ id, ok: true });
|
attempts.push({ id, ok: true, yol: route.yol });
|
||||||
return {
|
return {
|
||||||
result,
|
result,
|
||||||
model: { id, label: p.label, provider: p.provider, model: p.model() },
|
model: { id, label: p.label, provider: p.provider, model: route.model, yol: route.yol },
|
||||||
attempts,
|
attempts,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
attempts.push({ id, ok: false, error: e.name === 'AbortError' ? 'zaman aşımı' : e.message });
|
attempts.push({ id, ok: false, yol: route.yol, error: e.name === 'AbortError' ? 'zaman aşımı' : e.message });
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,11 +115,11 @@ export const AiSpecAnalyzerModal = () => {
|
|||||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4 flex gap-3">
|
<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" />
|
<Info className="w-4 h-4 text-amber-700 shrink-0 mt-0.5" />
|
||||||
<p className="text-xs text-amber-900 leading-relaxed">
|
<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
|
<strong>AI henüz yapılandırılmamış.</strong> En kolayı: sunucuya tek bir{' '}
|
||||||
(<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">OPENROUTER_API_KEY</code> ekleyin —
|
||||||
{' '}<code className="bg-amber-100 px-1 rounded">GEMINI_API_KEY</code> …) ve yeniden dağıtın.
|
zincirdeki tüm modeller (GLM, DeepSeek, Gemini, Grok, GPT, Claude) bu tek anahtarla çalışır.
|
||||||
Model sırası, Admin → AI Model Ayarları'ndan belirlenir.
|
Dilerseniz tek tek sağlayıcı anahtarı da verebilirsiniz; doğrudan anahtar OpenRouter'ı ezer.
|
||||||
</p>
|
Model sırası Admin → AI Model Ayarları'ndan belirlenir.</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useApp } from '../../context/AppContext';
|
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';
|
import confetti from 'canvas-confetti';
|
||||||
|
|
||||||
const RENK = {
|
const RENK = {
|
||||||
@@ -17,6 +18,10 @@ const SIRA_ETIKET = ['Birincil (Primary)', '2. Yedek (Fallback)', '3. Yedek (Fal
|
|||||||
export const AiModelSettings = () => {
|
export const AiModelSettings = () => {
|
||||||
const { AI_MODEL_CATALOG, aiModelConfig, setAiModelConfig, getModelById } = useApp();
|
const { AI_MODEL_CATALOG, aiModelConfig, setAiModelConfig, getModelById } = useApp();
|
||||||
const [saved, setSaved] = useState(false);
|
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 chain = aiModelConfig.chain;
|
||||||
const available = AI_MODEL_CATALOG.filter(m => !chain.includes(m.id));
|
const available = AI_MODEL_CATALOG.filter(m => !chain.includes(m.id));
|
||||||
@@ -58,6 +63,30 @@ export const AiModelSettings = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 animate-fade-in">
|
<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 */}
|
{/* 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="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">
|
<div className="flex items-center gap-3">
|
||||||
@@ -117,7 +146,10 @@ export const AiModelSettings = () => {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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 className="text-[10px] text-slate-400 mt-0.5">{SIRA_ETIKET[idx] || `${idx + 1}. sırada`}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -192,12 +192,12 @@ export const AppProvider = ({ children }) => {
|
|||||||
// AI Model Yönetimi (admin panelden seçilebilir + fallback zinciri)
|
// AI Model Yönetimi (admin panelden seçilebilir + fallback zinciri)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
const AI_MODEL_CATALOG = [
|
const AI_MODEL_CATALOG = [
|
||||||
{ id: 'glm', label: 'GLM 5.3', provider: 'Z.ai', alias: '~z-ai/glm-latest', model: 'glm-5.3', renk: 'blue' },
|
{ id: 'glm', label: 'GLM Latest', provider: 'Z.ai', or: '~z-ai/glm-latest', model: 'glm-5.3', renk: 'blue' },
|
||||||
{ id: 'deepseek', label: 'DeepSeek V4 Flash', provider: 'DeepSeek', alias: '~deepseek/deepseek-v4-flash-latest', model: 'deepseek-v4-flash-0731', renk: 'indigo' },
|
{ id: 'deepseek', label: 'DeepSeek V4 Flash Latest', provider: 'DeepSeek', or: '~deepseek/deepseek-v4-flash-latest', model: 'deepseek-chat', renk: 'indigo' },
|
||||||
{ id: 'gemini', label: 'Gemini 3.7 Flash', provider: 'Google', alias: '~google/gemini-flash-latest', model: 'gemini-3.7-flash', renk: 'amber' },
|
{ id: 'gemini', label: 'Gemini Flash Latest', provider: 'Google', or: '~google/gemini-flash-latest', model: 'gemini-flash-latest', renk: 'amber' },
|
||||||
{ id: 'grok', label: 'Grok 4.6', provider: 'xAI', alias: '~x-ai/grok-latest', model: 'grok-4.6', renk: 'slate' },
|
{ id: 'grok', label: 'Grok Latest', provider: 'xAI', or: '~x-ai/grok-latest', model: 'grok-4.6', renk: 'slate' },
|
||||||
{ id: 'gpt', label: 'GPT-5.6 Sol', provider: 'OpenAI', alias: '~openai/gpt-latest', model: 'gpt-5.6-sol', renk: 'emerald' },
|
{ id: 'gpt', label: 'GPT Chat Latest', provider: 'OpenAI', or: 'openai/gpt-chat-latest', model: 'gpt-5.6-sol', renk: 'emerald' },
|
||||||
{ id: 'claude', label: 'Claude Opus 5', provider: 'Anthropic', alias: '~anthropic/claude-opus-latest', model: 'claude-opus-5', renk: 'orange' }
|
{ id: 'claude', label: 'Claude Opus Latest', provider: 'Anthropic', or: '~anthropic/claude-opus-latest', model: 'claude-opus-5', renk: 'orange' }
|
||||||
];
|
];
|
||||||
|
|
||||||
// Varsayılan fallback zinciri: GLM (birincil) → DeepSeek → Gemini
|
// Varsayılan fallback zinciri: GLM (birincil) → DeepSeek → Gemini
|
||||||
|
|||||||
Reference in New Issue
Block a user