+19
-0
@@ -15,2 +15,21 @@ /** | ||
| } | ||
| export interface ProviderMeta { | ||
| name: string; | ||
| label: string; | ||
| emoji: string; | ||
| category: 'free_local' | 'domestic' | 'overseas'; | ||
| description: string; | ||
| website: string; | ||
| } | ||
| export interface ModelEntry { | ||
| name: string; | ||
| provider: string; | ||
| type: 'llm' | 'embedding'; | ||
| inputPricePer1M: number; | ||
| outputPricePer1M: number; | ||
| currency: string; | ||
| contextWindow: number; | ||
| bestFor: string; | ||
| capabilities: string[]; | ||
| } | ||
| export declare class KitsUI { | ||
@@ -17,0 +36,0 @@ private config; |
+267
-35
@@ -18,30 +18,107 @@ /** | ||
| import { checkProvider } from '../health.js'; | ||
| // Model catalog for /api/models | ||
| const PROVIDER_META = [ | ||
| { name: 'ollama', label: 'Ollama', emoji: '๐ฆ', category: 'free_local', description: 'ๆฌๅฐๅ ่ดน่ฟ่กๅผๆบๆจกๅ / Run open-source models locally for free', website: 'https://ollama.com' }, | ||
| { name: 'deepseek', label: 'DeepSeek', emoji: '๐', category: 'domestic', description: '้ซๆงไปทๆฏๆจ็ๆจกๅ / Cost-effective reasoning models', website: 'https://deepseek.com' }, | ||
| { name: 'dashscope', label: '้ไนๅ้ฎ Qwen', emoji: 'โ๏ธ', category: 'domestic', description: '้ฟ้ไบๅคงๆจกๅ / Alibaba Cloud LLM', website: 'https://dashscope.aliyun.com' }, | ||
| { name: 'zhipu', label: 'ๆบ่ฐฑ GLM', emoji: '๐ง ', category: 'domestic', description: 'ๆธ ๅ็ณปๅคงๆจกๅ / Tsinghua-backed LLM', website: 'https://open.bigmodel.cn' }, | ||
| { name: 'moonshot', label: 'ๆไนๆ้ข Kimi', emoji: '๐', category: 'domestic', description: '้ฟไธไธๆๅคงๆจกๅ / Long-context LLM', website: 'https://moonshot.cn' }, | ||
| { name: 'minimax', label: 'MiniMax', emoji: '๐ต', category: 'domestic', description: 'ๅคๆจกๆๅคงๆจกๅ / Multimodal LLM', website: 'https://minimax.chat' }, | ||
| { name: 'yi', label: '้ถไธไธ็ฉ Yi', emoji: '1๏ธโฃ', category: 'domestic', description: 'ๆๅผๅคๅข้ๅคงๆจกๅ / Kai-Fu Lee LLM', website: 'https://01.ai' }, | ||
| { name: 'baichuan', label: '็พๅทๆบ่ฝ', emoji: '๐', category: 'domestic', description: '็พๅทๅคงๆจกๅ / Baichuan LLM', website: 'https://baichuan-ai.com' }, | ||
| { name: 'siliconflow', label: '็ก ๅบๆตๅจ', emoji: '๐', category: 'domestic', description: 'ๆจกๅๆจ็ๅนณๅฐ / Model inference platform', website: 'https://siliconflow.cn' }, | ||
| { name: 'stepfun', label: '้ถ่ทๆ่พฐ', emoji: 'โญ', category: 'domestic', description: 'ไธไบฟๅๆฐๅคงๆจกๅ / Trillion-parameter LLM', website: 'https://stepfun.com' }, | ||
| { name: 'openai', label: 'OpenAI', emoji: '๐ค', category: 'overseas', description: 'GPT ็ณปๅ / GPT series', website: 'https://openai.com' }, | ||
| { name: 'anthropic', label: 'Anthropic', emoji: '๐งฌ', category: 'overseas', description: 'Claude ็ณปๅ / Claude series', website: 'https://anthropic.com' }, | ||
| { name: 'gemini', label: 'Google Gemini', emoji: '๐ซ', category: 'overseas', description: 'Gemini ็ณปๅ / Gemini series', website: 'https://ai.google.dev' }, | ||
| { name: 'grok', label: 'xAI Grok', emoji: '๐', category: 'overseas', description: 'Grok ็ณปๅ / Grok series', website: 'https://x.ai' }, | ||
| { name: 'cohere', label: 'Cohere', emoji: '๐', category: 'overseas', description: 'Command ็ณปๅ / Command series', website: 'https://cohere.com' }, | ||
| { name: 'fireworks', label: 'Fireworks AI', emoji: '๐', category: 'overseas', description: 'ๅฟซ้ๆจ็ๅนณๅฐ / Fast inference platform', website: 'https://fireworks.ai' }, | ||
| { name: 'together', label: 'Together AI', emoji: '๐ค', category: 'overseas', description: 'ๅผๆบๆจกๅๆ็ฎก / Open-source model hosting', website: 'https://together.ai' }, | ||
| { name: 'groq', label: 'Groq', emoji: 'โก', category: 'overseas', description: '่ถ ไฝๅปถ่ฟๆจ็ / Ultra-low latency inference', website: 'https://groq.com' }, | ||
| { name: 'perplexity', label: 'Perplexity', emoji: '๐', category: 'overseas', description: 'ๆ็ดขๅขๅผบๆจกๅ / Search-augmented models', website: 'https://perplexity.ai' }, | ||
| { name: 'jina', label: 'Jina AI', emoji: '๐', category: 'overseas', description: 'Embedding ไธๅฎถ / Embedding specialist', website: 'https://jina.ai' }, | ||
| { name: 'voyage', label: 'Voyage AI', emoji: '๐ข', category: 'overseas', description: 'Embedding ไธๅฎถ / Embedding specialist', website: 'https://voyageai.com' }, | ||
| ]; | ||
| const MODEL_CATALOG = [ | ||
| { name: 'gpt-4o', provider: 'openai', inputPricePer1M: 2.50, outputPricePer1M: 10.00, contextWindow: 128000, bestFor: 'General, vision, coding' }, | ||
| { name: 'gpt-4o-mini', provider: 'openai', inputPricePer1M: 0.15, outputPricePer1M: 0.60, contextWindow: 128000, bestFor: 'Simple tasks, fast' }, | ||
| { name: 'text-embedding-3-small', provider: 'openai', inputPricePer1M: 0.02, outputPricePer1M: 0, contextWindow: 8191, bestFor: 'Embeddings' }, | ||
| { name: 'text-embedding-3-large', provider: 'openai', inputPricePer1M: 0.13, outputPricePer1M: 0, contextWindow: 8191, bestFor: 'High-dim embeddings' }, | ||
| { name: 'claude-3.5-sonnet', provider: 'anthropic', inputPricePer1M: 3.00, outputPricePer1M: 15.00, contextWindow: 200000, bestFor: 'Reasoning, coding' }, | ||
| { name: 'claude-3-haiku', provider: 'anthropic', inputPricePer1M: 0.25, outputPricePer1M: 1.25, contextWindow: 200000, bestFor: 'Fast, cheap' }, | ||
| { name: 'gemini-1.5-pro', provider: 'gemini', inputPricePer1M: 1.25, outputPricePer1M: 5.00, contextWindow: 2000000, bestFor: 'Huge context, analysis' }, | ||
| { name: 'gemini-2.5-flash', provider: 'gemini', inputPricePer1M: 0.15, outputPricePer1M: 0.60, contextWindow: 1000000, bestFor: 'Fast, cheap' }, | ||
| { name: 'deepseek-chat', provider: 'deepseek', inputPricePer1M: 0.14, outputPricePer1M: 0.28, contextWindow: 128000, bestFor: 'General, cheap GPT-4 class' }, | ||
| { name: 'deepseek-coder-v2', provider: 'deepseek', inputPricePer1M: 0.14, outputPricePer1M: 0.28, contextWindow: 128000, bestFor: 'Coding' }, | ||
| { name: 'deepseek-reasoner', provider: 'deepseek', inputPricePer1M: 0.55, outputPricePer1M: 2.19, contextWindow: 64000, bestFor: 'Deep reasoning' }, | ||
| { name: 'moonshot-v1-8k', provider: 'moonshot', inputPricePer1M: 0.17, outputPricePer1M: 0.17, contextWindow: 8000, bestFor: 'Chinese LLM' }, | ||
| { name: 'glm-4-flash', provider: 'zhipu', inputPricePer1M: 0.01, outputPricePer1M: 0.01, contextWindow: 128000, bestFor: 'Near-free Chinese' }, | ||
| { name: 'glm-4-plus', provider: 'zhipu', inputPricePer1M: 7.00, outputPricePer1M: 7.00, contextWindow: 128000, bestFor: 'Premium Chinese' }, | ||
| { name: 'qwen-turbo', provider: 'dashscope', inputPricePer1M: 0.04, outputPricePer1M: 0.08, contextWindow: 128000, bestFor: 'Fast, cheap Alibaba' }, | ||
| { name: 'qwen-plus', provider: 'dashscope', inputPricePer1M: 0.11, outputPricePer1M: 0.28, contextWindow: 128000, bestFor: 'Balanced Alibaba' }, | ||
| { name: 'qwen2.5', provider: 'ollama', inputPricePer1M: 0, outputPricePer1M: 0, contextWindow: 32000, bestFor: 'Free local multilingual' }, | ||
| { name: 'llama3', provider: 'ollama', inputPricePer1M: 0, outputPricePer1M: 0, contextWindow: 8000, bestFor: 'Free local general' }, | ||
| { name: 'deepseek-coder-v2:local', provider: 'ollama', inputPricePer1M: 0, outputPricePer1M: 0, contextWindow: 128000, bestFor: 'Free local coding' }, | ||
| { name: 'nomic-embed-text', provider: 'ollama', inputPricePer1M: 0, outputPricePer1M: 0, contextWindow: 8192, bestFor: 'Free local embeddings' }, | ||
| // OpenAI LLM | ||
| { name: 'gpt-4o', provider: 'openai', type: 'llm', inputPricePer1M: 2.50, outputPricePer1M: 10.00, currency: 'USD', contextWindow: 128000, bestFor: 'General, vision, coding', capabilities: ['vision', 'function_call', 'streaming'] }, | ||
| { name: 'gpt-4o-mini', provider: 'openai', type: 'llm', inputPricePer1M: 0.15, outputPricePer1M: 0.60, currency: 'USD', contextWindow: 128000, bestFor: 'Simple tasks, fast', capabilities: ['vision', 'function_call', 'streaming'] }, | ||
| // Anthropic LLM | ||
| { name: 'claude-3.5-sonnet', provider: 'anthropic', type: 'llm', inputPricePer1M: 3.00, outputPricePer1M: 15.00, currency: 'USD', contextWindow: 200000, bestFor: 'Reasoning, coding', capabilities: ['vision', 'function_call', 'streaming'] }, | ||
| { name: 'claude-3-haiku', provider: 'anthropic', type: 'llm', inputPricePer1M: 0.25, outputPricePer1M: 1.25, currency: 'USD', contextWindow: 200000, bestFor: 'Fast, cheap', capabilities: ['streaming'] }, | ||
| // Gemini LLM | ||
| { name: 'gemini-2.5-pro', provider: 'gemini', type: 'llm', inputPricePer1M: 1.25, outputPricePer1M: 10.00, currency: 'USD', contextWindow: 2000000, bestFor: 'Huge context, analysis', capabilities: ['vision', 'function_call', 'streaming'] }, | ||
| { name: 'gemini-2.5-flash', provider: 'gemini', type: 'llm', inputPricePer1M: 0.15, outputPricePer1M: 0.60, currency: 'USD', contextWindow: 1000000, bestFor: 'Fast, cheap', capabilities: ['vision', 'function_call', 'streaming'] }, | ||
| // DeepSeek LLM | ||
| { name: 'deepseek-chat', provider: 'deepseek', type: 'llm', inputPricePer1M: 0.14, outputPricePer1M: 0.28, currency: 'USD', contextWindow: 128000, bestFor: 'General, cheap GPT-4 class', capabilities: ['function_call', 'streaming'] }, | ||
| { name: 'deepseek-coder-v2', provider: 'deepseek', type: 'llm', inputPricePer1M: 0.14, outputPricePer1M: 0.28, currency: 'USD', contextWindow: 128000, bestFor: 'Coding', capabilities: ['function_call', 'streaming'] }, | ||
| { name: 'deepseek-reasoner', provider: 'deepseek', type: 'llm', inputPricePer1M: 0.55, outputPricePer1M: 2.19, currency: 'USD', contextWindow: 64000, bestFor: 'Deep reasoning', capabilities: ['streaming'] }, | ||
| // DashScope (Qwen) LLM | ||
| { name: 'qwen-max', provider: 'dashscope', type: 'llm', inputPricePer1M: 2.40, outputPricePer1M: 9.60, currency: 'CNY', contextWindow: 128000, bestFor: 'Premium Chinese LLM', capabilities: ['function_call', 'streaming'] }, | ||
| { name: 'qwen-plus', provider: 'dashscope', type: 'llm', inputPricePer1M: 0.80, outputPricePer1M: 2.00, currency: 'CNY', contextWindow: 128000, bestFor: 'Balanced Alibaba', capabilities: ['function_call', 'streaming'] }, | ||
| { name: 'qwen-turbo', provider: 'dashscope', type: 'llm', inputPricePer1M: 0.30, outputPricePer1M: 0.60, currency: 'CNY', contextWindow: 128000, bestFor: 'Fast, cheap Alibaba', capabilities: ['function_call', 'streaming'] }, | ||
| // Zhipu (GLM) LLM | ||
| { name: 'glm-4-plus', provider: 'zhipu', type: 'llm', inputPricePer1M: 50.00, outputPricePer1M: 50.00, currency: 'CNY', contextWindow: 128000, bestFor: 'Premium Chinese', capabilities: ['function_call', 'streaming'] }, | ||
| { name: 'glm-4-flash', provider: 'zhipu', type: 'llm', inputPricePer1M: 0.10, outputPricePer1M: 0.10, currency: 'CNY', contextWindow: 128000, bestFor: 'Near-free Chinese', capabilities: ['function_call', 'streaming'] }, | ||
| // Moonshot LLM | ||
| { name: 'moonshot-v1-auto', provider: 'moonshot', type: 'llm', inputPricePer1M: 12.00, outputPricePer1M: 12.00, currency: 'CNY', contextWindow: 128000, bestFor: 'Long context Chinese', capabilities: ['streaming'] }, | ||
| // MiniMax LLM | ||
| { name: 'MiniMax-Text-01', provider: 'minimax', type: 'llm', inputPricePer1M: 1.00, outputPricePer1M: 8.00, currency: 'CNY', contextWindow: 1000000, bestFor: 'Long context multimodal', capabilities: ['streaming'] }, | ||
| // Grok LLM | ||
| { name: 'grok-3', provider: 'grok', type: 'llm', inputPricePer1M: 3.00, outputPricePer1M: 15.00, currency: 'USD', contextWindow: 131072, bestFor: 'General reasoning', capabilities: ['function_call', 'streaming'] }, | ||
| // Cohere LLM | ||
| { name: 'command-r-plus', provider: 'cohere', type: 'llm', inputPricePer1M: 2.50, outputPricePer1M: 10.00, currency: 'USD', contextWindow: 128000, bestFor: 'RAG, enterprise', capabilities: ['function_call', 'streaming'] }, | ||
| // Yi LLM | ||
| { name: 'yi-large', provider: 'yi', type: 'llm', inputPricePer1M: 20.00, outputPricePer1M: 20.00, currency: 'CNY', contextWindow: 32768, bestFor: 'Chinese reasoning', capabilities: ['streaming'] }, | ||
| // Baichuan LLM | ||
| { name: 'Baichuan4', provider: 'baichuan', type: 'llm', inputPricePer1M: 100.00, outputPricePer1M: 100.00, currency: 'CNY', contextWindow: 128000, bestFor: 'Premium Chinese', capabilities: ['streaming'] }, | ||
| // SiliconFlow LLM | ||
| { name: 'deepseek-ai/DeepSeek-V3', provider: 'siliconflow', type: 'llm', inputPricePer1M: 2.00, outputPricePer1M: 8.00, currency: 'CNY', contextWindow: 128000, bestFor: 'DeepSeek via SiliconFlow', capabilities: ['streaming'] }, | ||
| // StepFun LLM | ||
| { name: 'step-2-16k', provider: 'stepfun', type: 'llm', inputPricePer1M: 38.00, outputPricePer1M: 120.00, currency: 'CNY', contextWindow: 16000, bestFor: 'Premium reasoning', capabilities: ['streaming'] }, | ||
| // Fireworks LLM | ||
| { name: 'llama-v3p1-70b-instruct', provider: 'fireworks', type: 'llm', inputPricePer1M: 0.90, outputPricePer1M: 0.90, currency: 'USD', contextWindow: 131072, bestFor: 'Open-source hosting', capabilities: ['function_call', 'streaming'] }, | ||
| // Together LLM | ||
| { name: 'Meta-Llama-3.1-70B-Instruct-Turbo', provider: 'together', type: 'llm', inputPricePer1M: 0.88, outputPricePer1M: 0.88, currency: 'USD', contextWindow: 131072, bestFor: 'Open-source hosting', capabilities: ['function_call', 'streaming'] }, | ||
| // Groq LLM | ||
| { name: 'llama-3.3-70b-versatile', provider: 'groq', type: 'llm', inputPricePer1M: 0.59, outputPricePer1M: 0.79, currency: 'USD', contextWindow: 131072, bestFor: 'Ultra-fast inference', capabilities: ['function_call', 'streaming'] }, | ||
| // Perplexity LLM | ||
| { name: 'sonar-pro', provider: 'perplexity', type: 'llm', inputPricePer1M: 3.00, outputPricePer1M: 15.00, currency: 'USD', contextWindow: 128000, bestFor: 'Search-augmented', capabilities: ['streaming'] }, | ||
| // Ollama LLM | ||
| { name: 'qwen2.5', provider: 'ollama', type: 'llm', inputPricePer1M: 0, outputPricePer1M: 0, currency: 'USD', contextWindow: 32000, bestFor: 'Free local multilingual', capabilities: ['streaming'] }, | ||
| { name: 'llama3', provider: 'ollama', type: 'llm', inputPricePer1M: 0, outputPricePer1M: 0, currency: 'USD', contextWindow: 8000, bestFor: 'Free local general', capabilities: ['streaming'] }, | ||
| { name: 'deepseek-coder-v2:local', provider: 'ollama', type: 'llm', inputPricePer1M: 0, outputPricePer1M: 0, currency: 'USD', contextWindow: 128000, bestFor: 'Free local coding', capabilities: ['streaming'] }, | ||
| // โโ Embedding Models โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | ||
| { name: 'text-embedding-3-large', provider: 'openai', type: 'embedding', inputPricePer1M: 0.13, outputPricePer1M: 0, currency: 'USD', contextWindow: 8191, bestFor: 'High-dim embeddings', capabilities: [] }, | ||
| { name: 'text-embedding-3-small', provider: 'openai', type: 'embedding', inputPricePer1M: 0.02, outputPricePer1M: 0, currency: 'USD', contextWindow: 8191, bestFor: 'Cheap embeddings', capabilities: [] }, | ||
| { name: 'gemini-embedding-001', provider: 'gemini', type: 'embedding', inputPricePer1M: 0, outputPricePer1M: 0, currency: 'USD', contextWindow: 3072, bestFor: 'Free embeddings', capabilities: [] }, | ||
| { name: 'deepseek-embedding-v2', provider: 'deepseek', type: 'embedding', inputPricePer1M: 0.07, outputPricePer1M: 0, currency: 'USD', contextWindow: 8192, bestFor: 'Cheap embeddings', capabilities: [] }, | ||
| { name: 'text-embedding-v3', provider: 'dashscope', type: 'embedding', inputPricePer1M: 0.70, outputPricePer1M: 0, currency: 'CNY', contextWindow: 8192, bestFor: 'Chinese embeddings', capabilities: [] }, | ||
| { name: 'embedding-3', provider: 'zhipu', type: 'embedding', inputPricePer1M: 0.50, outputPricePer1M: 0, currency: 'CNY', contextWindow: 8192, bestFor: 'Chinese embeddings', capabilities: [] }, | ||
| { name: 'embed-english-v3.0', provider: 'cohere', type: 'embedding', inputPricePer1M: 0.10, outputPricePer1M: 0, currency: 'USD', contextWindow: 512, bestFor: 'English embeddings', capabilities: [] }, | ||
| { name: 'jina-embeddings-v3', provider: 'jina', type: 'embedding', inputPricePer1M: 0.02, outputPricePer1M: 0, currency: 'USD', contextWindow: 8192, bestFor: 'Multilingual embeddings', capabilities: [] }, | ||
| { name: 'voyage-3', provider: 'voyage', type: 'embedding', inputPricePer1M: 0.06, outputPricePer1M: 0, currency: 'USD', contextWindow: 32000, bestFor: 'Code & text embeddings', capabilities: [] }, | ||
| { name: 'nomic-embed-text', provider: 'ollama', type: 'embedding', inputPricePer1M: 0, outputPricePer1M: 0, currency: 'USD', contextWindow: 8192, bestFor: 'Free local embeddings', capabilities: [] }, | ||
| ]; | ||
| const PROVIDERS = ['openai', 'anthropic', 'gemini', 'deepseek', 'moonshot', 'zhipu', 'ollama', 'dashscope']; | ||
| // โโ In-memory state โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | ||
| const apiKeys = new Map(); | ||
| let budgetLimit = 100; // USD | ||
| const usageStats = { | ||
| today: { tokens: 45200, cost: 0.12 }, | ||
| week: { tokens: 312500, cost: 0.85 }, | ||
| month: { tokens: 1250000, cost: 3.42 }, | ||
| byModel: [ | ||
| { model: 'deepseek-chat', provider: 'deepseek', tokens: 850000, cost: 1.19 }, | ||
| { model: 'gpt-4o-mini', provider: 'openai', tokens: 250000, cost: 0.45 }, | ||
| { model: 'gemini-2.5-flash', provider: 'gemini', tokens: 100000, cost: 0.12 }, | ||
| { model: 'text-embedding-3-small', provider: 'openai', tokens: 50000, cost: 0.001 }, | ||
| ], | ||
| activeChat: 'deepseek-chat', | ||
| activeEmbedding: 'text-embedding-3-small', | ||
| }; | ||
| // โโ Helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | ||
| function corsHeaders() { | ||
| return { | ||
| 'Access-Control-Allow-Origin': '*', | ||
| 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', | ||
| 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', | ||
| 'Access-Control-Allow-Headers': 'Content-Type', | ||
@@ -63,2 +140,14 @@ 'Content-Type': 'application/json', | ||
| } | ||
| function maskKey(key) { | ||
| if (key.length <= 8) | ||
| return key.slice(0, 4) + '****'; | ||
| return key.slice(0, 4) + '****' + key.slice(-4); | ||
| } | ||
| function getProviderModels(providerName) { | ||
| return MODEL_CATALOG.filter(m => m.provider === providerName); | ||
| } | ||
| function getProviderMeta(providerName) { | ||
| return PROVIDER_META.find(p => p.name === providerName); | ||
| } | ||
| // โโ Main Class โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | ||
| export class KitsUI { | ||
@@ -77,3 +166,2 @@ config; | ||
| this.server = http.createServer(async (req, res) => { | ||
| // Handle CORS preflight | ||
| if (req.method === 'OPTIONS') { | ||
@@ -87,6 +175,16 @@ res.writeHead(204, corsHeaders()); | ||
| try { | ||
| // API routes | ||
| // โโ API Routes โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | ||
| // GET /api/models | ||
| if (pathname === '/api/models' && req.method === 'GET') { | ||
| jsonResponse(res, { models: MODEL_CATALOG, total: MODEL_CATALOG.length }); | ||
| } | ||
| // GET /api/stats | ||
| else if (pathname === '/api/stats' && req.method === 'GET') { | ||
| jsonResponse(res, { | ||
| ...usageStats, | ||
| budget: budgetLimit, | ||
| budgetUsedPercent: Math.round((usageStats.month.cost / budgetLimit) * 100), | ||
| }); | ||
| } | ||
| // GET /api/recommend | ||
| else if (pathname === '/api/recommend' && req.method === 'GET') { | ||
@@ -98,15 +196,65 @@ const task = (parsedUrl.searchParams.get('task') || 'chat'); | ||
| } | ||
| // GET /api/cost | ||
| else if (pathname === '/api/cost' && req.method === 'GET') { | ||
| const model = parsedUrl.searchParams.get('model') || 'gpt-4o'; | ||
| const tokens = parseInt(parsedUrl.searchParams.get('tokens') || '1000', 10); | ||
| const estimate = estimateModelCost(model, tokens, tokens); | ||
| jsonResponse(res, { model, tokens, estimate }); | ||
| const model = parsedUrl.searchParams.get('model'); | ||
| if (model) { | ||
| const tokens = parseInt(parsedUrl.searchParams.get('tokens') || '1000', 10); | ||
| const estimate = estimateModelCost(model, tokens, tokens); | ||
| jsonResponse(res, { model, tokens, estimate }); | ||
| } | ||
| else { | ||
| jsonResponse(res, { | ||
| today: usageStats.today, | ||
| week: usageStats.week, | ||
| month: usageStats.month, | ||
| byModel: usageStats.byModel, | ||
| budget: budgetLimit, | ||
| }); | ||
| } | ||
| } | ||
| // GET /api/cost/trend | ||
| else if (pathname === '/api/cost/trend' && req.method === 'GET') { | ||
| const trend = []; | ||
| const now = new Date(); | ||
| for (let i = 29; i >= 0; i--) { | ||
| const d = new Date(now); | ||
| d.setDate(d.getDate() - i); | ||
| const dateStr = d.toISOString().slice(0, 10); | ||
| const tokens = Math.floor(30000 + Math.random() * 50000); | ||
| const cost = parseFloat((tokens * 0.000003).toFixed(4)); | ||
| trend.push({ date: dateStr, tokens, cost }); | ||
| } | ||
| jsonResponse(res, { trend }); | ||
| } | ||
| // PUT /api/cost/budget | ||
| else if (pathname === '/api/cost/budget' && req.method === 'PUT') { | ||
| const body = JSON.parse(await readBody(req)); | ||
| if (typeof body.budget === 'number' && body.budget > 0) { | ||
| budgetLimit = body.budget; | ||
| jsonResponse(res, { budget: budgetLimit, message: 'Budget updated' }); | ||
| } | ||
| else { | ||
| jsonResponse(res, { error: 'Invalid budget value' }, 400); | ||
| } | ||
| } | ||
| // GET /api/providers | ||
| else if (pathname === '/api/providers' && req.method === 'GET') { | ||
| const providers = PROVIDERS.map(p => ({ | ||
| name: p, | ||
| models: MODEL_CATALOG.filter(m => m.provider === p).length, | ||
| })); | ||
| const providers = PROVIDER_META.map(p => { | ||
| const models = getProviderModels(p.name); | ||
| const llmModels = models.filter(m => m.type === 'llm'); | ||
| const embModels = models.filter(m => m.type === 'embedding'); | ||
| const prices = models.filter(m => m.inputPricePer1M > 0).map(m => m.inputPricePer1M); | ||
| return { | ||
| ...p, | ||
| models: models.length, | ||
| llmCount: llmModels.length, | ||
| embeddingCount: embModels.length, | ||
| priceRange: prices.length > 0 | ||
| ? { min: Math.min(...prices), max: Math.max(...prices), currency: models[0]?.currency || 'USD' } | ||
| : { min: 0, max: 0, currency: 'USD' }, | ||
| }; | ||
| }); | ||
| jsonResponse(res, { providers }); | ||
| } | ||
| // GET /api/providers/check | ||
| else if (pathname === '/api/providers/check' && req.method === 'GET') { | ||
@@ -117,2 +265,85 @@ const name = parsedUrl.searchParams.get('name') || ''; | ||
| } | ||
| // GET /api/providers/:name | ||
| else if (pathname.match(/^\/api\/providers\/[^/]+$/) && !pathname.includes('/models') && !pathname.includes('/check') && req.method === 'GET') { | ||
| const name = pathname.split('/')[3]; | ||
| const meta = getProviderMeta(name); | ||
| if (!meta) { | ||
| jsonResponse(res, { error: 'Provider not found' }, 404); | ||
| } | ||
| else { | ||
| const models = getProviderModels(name); | ||
| jsonResponse(res, { provider: meta, models }); | ||
| } | ||
| } | ||
| // GET /api/providers/:name/models | ||
| else if (pathname.match(/^\/api\/providers\/[^/]+\/models$/) && req.method === 'GET') { | ||
| const name = pathname.split('/')[3]; | ||
| const models = getProviderModels(name); | ||
| jsonResponse(res, { models }); | ||
| } | ||
| // GET /api/keys | ||
| else if (pathname === '/api/keys' && req.method === 'GET') { | ||
| const keys = []; | ||
| // Check env vars | ||
| const envKeyMap = { | ||
| openai: 'OPENAI_API_KEY', anthropic: 'ANTHROPIC_API_KEY', gemini: 'GEMINI_API_KEY', | ||
| deepseek: 'DEEPSEEK_API_KEY', moonshot: 'MOONSHOT_API_KEY', zhipu: 'ZHIPU_API_KEY', | ||
| dashscope: 'DASHSCOPE_API_KEY', minimax: 'MINIMAX_API_KEY', yi: 'YI_API_KEY', | ||
| baichuan: 'BAICHUAN_API_KEY', siliconflow: 'SILICONFLOW_API_KEY', stepfun: 'STEPFUN_API_KEY', | ||
| fireworks: 'FIREWORKS_API_KEY', together: 'TOGETHER_API_KEY', groq: 'GROQ_API_KEY', | ||
| perplexity: 'PPLX_API_KEY', cohere: 'COHERE_API_KEY', jina: 'JINA_API_KEY', | ||
| voyage: 'VOYAGE_API_KEY', grok: 'GROK_API_KEY', | ||
| }; | ||
| for (const [prov, envName] of Object.entries(envKeyMap)) { | ||
| const envVal = process.env[envName]; | ||
| if (envVal) { | ||
| keys.push({ provider: prov, masked: maskKey(envVal), addedAt: 'env', status: 'configured' }); | ||
| } | ||
| } | ||
| // Session keys | ||
| for (const [prov, data] of apiKeys.entries()) { | ||
| keys.push({ provider: prov, masked: maskKey(data.key), addedAt: data.addedAt, status: 'session' }); | ||
| } | ||
| jsonResponse(res, { keys }); | ||
| } | ||
| // POST /api/keys | ||
| else if (pathname === '/api/keys' && req.method === 'POST') { | ||
| const body = JSON.parse(await readBody(req)); | ||
| if (!body.provider || !body.key) { | ||
| jsonResponse(res, { error: 'provider and key are required' }, 400); | ||
| } | ||
| else { | ||
| apiKeys.set(body.provider, { key: body.key, addedAt: new Date().toISOString() }); | ||
| jsonResponse(res, { message: 'Key added', provider: body.provider }); | ||
| } | ||
| } | ||
| // DELETE /api/keys/:provider | ||
| else if (pathname.match(/^\/api\/keys\/[^/]+$/) && req.method === 'DELETE') { | ||
| const provider = pathname.split('/')[3]; | ||
| const deleted = apiKeys.delete(provider); | ||
| jsonResponse(res, { deleted, provider }); | ||
| } | ||
| // POST /api/keys/test | ||
| else if (pathname === '/api/keys/test' && req.method === 'POST') { | ||
| const body = JSON.parse(await readBody(req)); | ||
| const provider = body.provider || ''; | ||
| const result = await checkProvider(provider); | ||
| jsonResponse(res, { provider, ...result }); | ||
| } | ||
| // POST /api/keys/test-all | ||
| else if (pathname === '/api/keys/test-all' && req.method === 'POST') { | ||
| const allProviders = [...new Set(MODEL_CATALOG.map(m => m.provider))]; | ||
| const results = []; | ||
| for (const p of allProviders) { | ||
| try { | ||
| const r = await checkProvider(p); | ||
| results.push({ provider: p, ...r }); | ||
| } | ||
| catch (e) { | ||
| results.push({ provider: p, available: false, latencyMs: 0, error: e.message }); | ||
| } | ||
| } | ||
| jsonResponse(res, { results }); | ||
| } | ||
| // GET /api/brain/status | ||
| else if (pathname === '/api/brain/status' && req.method === 'GET') { | ||
@@ -125,2 +356,3 @@ jsonResponse(res, { | ||
| } | ||
| // POST /api/test | ||
| else if (pathname === '/api/test' && req.method === 'POST') { | ||
@@ -135,4 +367,4 @@ const body = JSON.parse(await readBody(req)); | ||
| } | ||
| else if (pathname === '/' || pathname === '/index.html') { | ||
| // Serve HTML | ||
| // Serve index.html for all non-API routes (SPA) | ||
| else if (!pathname.startsWith('/api/')) { | ||
| const htmlPath = path.join(this.config.staticDir, 'index.html'); | ||
@@ -139,0 +371,0 @@ try { |
+154
-10
@@ -31,3 +31,3 @@ import { describe, it, expect, afterAll } from 'vitest'; | ||
| }); | ||
| it('/api/providers returns providers', async () => { | ||
| it('/api/providers returns providers with metadata', async () => { | ||
| const res = await fetch(`http://localhost:${PORT}/api/providers`); | ||
@@ -39,4 +39,44 @@ const data = await res.json(); | ||
| expect(data.providers[0]).toHaveProperty('name'); | ||
| expect(data.providers[0]).toHaveProperty('models'); | ||
| expect(data.providers[0]).toHaveProperty('emoji'); | ||
| expect(data.providers[0]).toHaveProperty('category'); | ||
| expect(data.providers[0]).toHaveProperty('llmCount'); | ||
| expect(data.providers[0]).toHaveProperty('embeddingCount'); | ||
| expect(data.providers[0]).toHaveProperty('priceRange'); | ||
| }); | ||
| it('/api/providers/:name returns provider detail', async () => { | ||
| const res = await fetch(`http://localhost:${PORT}/api/providers/openai`); | ||
| const data = await res.json(); | ||
| expect(res.status).toBe(200); | ||
| expect(data.provider).toBeDefined(); | ||
| expect(data.provider.name).toBe('openai'); | ||
| expect(data.models).toBeDefined(); | ||
| expect(data.models.length).toBeGreaterThan(0); | ||
| }); | ||
| it('/api/providers/:name returns 404 for unknown', async () => { | ||
| const res = await fetch(`http://localhost:${PORT}/api/providers/nonexistent`); | ||
| const data = await res.json(); | ||
| expect(res.status).toBe(404); | ||
| expect(data.error).toBe('Provider not found'); | ||
| }); | ||
| it('/api/providers/:name/models returns models array', async () => { | ||
| const res = await fetch(`http://localhost:${PORT}/api/providers/deepseek/models`); | ||
| const data = await res.json(); | ||
| expect(res.status).toBe(200); | ||
| expect(data.models).toBeDefined(); | ||
| expect(data.models.length).toBeGreaterThan(0); | ||
| expect(data.models[0].provider).toBe('deepseek'); | ||
| }); | ||
| it('/api/stats returns usage stats', async () => { | ||
| const res = await fetch(`http://localhost:${PORT}/api/stats`); | ||
| const data = await res.json(); | ||
| expect(res.status).toBe(200); | ||
| expect(data.today).toBeDefined(); | ||
| expect(data.week).toBeDefined(); | ||
| expect(data.month).toBeDefined(); | ||
| expect(data.byModel).toBeDefined(); | ||
| expect(data.budget).toBeGreaterThan(0); | ||
| expect(data.budgetUsedPercent).toBeDefined(); | ||
| expect(data.activeChat).toBeDefined(); | ||
| expect(data.activeEmbedding).toBeDefined(); | ||
| }); | ||
| it('/api/recommend returns recommendations', async () => { | ||
@@ -50,7 +90,113 @@ const res = await fetch(`http://localhost:${PORT}/api/recommend?task=coding&budget=low`); | ||
| }); | ||
| it('returns 404 for unknown routes', async () => { | ||
| it('/api/cost returns cost data without model param', async () => { | ||
| const res = await fetch(`http://localhost:${PORT}/api/cost`); | ||
| const data = await res.json(); | ||
| expect(res.status).toBe(200); | ||
| expect(data.today).toBeDefined(); | ||
| expect(data.month).toBeDefined(); | ||
| expect(data.byModel).toBeDefined(); | ||
| expect(data.budget).toBeGreaterThan(0); | ||
| }); | ||
| it('/api/cost returns estimate with model param', async () => { | ||
| const res = await fetch(`http://localhost:${PORT}/api/cost?model=gpt-4o&tokens=1000`); | ||
| const data = await res.json(); | ||
| expect(res.status).toBe(200); | ||
| expect(data.estimate).toBeDefined(); | ||
| expect(data.estimate.cost).toBeGreaterThan(0); | ||
| }); | ||
| it('/api/cost/trend returns 30-day trend data', async () => { | ||
| const res = await fetch(`http://localhost:${PORT}/api/cost/trend`); | ||
| const data = await res.json(); | ||
| expect(res.status).toBe(200); | ||
| expect(data.trend).toBeDefined(); | ||
| expect(data.trend.length).toBe(30); | ||
| expect(data.trend[0]).toHaveProperty('date'); | ||
| expect(data.trend[0]).toHaveProperty('tokens'); | ||
| expect(data.trend[0]).toHaveProperty('cost'); | ||
| }); | ||
| it('PUT /api/cost/budget updates budget', async () => { | ||
| const res = await fetch(`http://localhost:${PORT}/api/cost/budget`, { | ||
| method: 'PUT', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ budget: 200 }), | ||
| }); | ||
| const data = await res.json(); | ||
| expect(res.status).toBe(200); | ||
| expect(data.budget).toBe(200); | ||
| // Verify it persists | ||
| const statsRes = await fetch(`http://localhost:${PORT}/api/stats`); | ||
| const stats = await statsRes.json(); | ||
| expect(stats.budget).toBe(200); | ||
| }); | ||
| it('PUT /api/cost/budget rejects invalid value', async () => { | ||
| const res = await fetch(`http://localhost:${PORT}/api/cost/budget`, { | ||
| method: 'PUT', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ budget: -5 }), | ||
| }); | ||
| expect(res.status).toBe(400); | ||
| }); | ||
| it('/api/keys CRUD operations', async () => { | ||
| // GET - initially may have env keys | ||
| const listRes = await fetch(`http://localhost:${PORT}/api/keys`); | ||
| const listData = await listRes.json(); | ||
| expect(listRes.status).toBe(200); | ||
| expect(listData.keys).toBeDefined(); | ||
| // POST - add a session key | ||
| const addRes = await fetch(`http://localhost:${PORT}/api/keys`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ provider: 'testprov', key: 'sk-test1234567890' }), | ||
| }); | ||
| const addData = await addRes.json(); | ||
| expect(addRes.status).toBe(200); | ||
| expect(addData.provider).toBe('testprov'); | ||
| // GET - verify it's there | ||
| const list2 = await (await fetch(`http://localhost:${PORT}/api/keys`)).json(); | ||
| const found = list2.keys.find((k) => k.provider === 'testprov'); | ||
| expect(found).toBeDefined(); | ||
| expect(found.masked).toContain('sk-t'); | ||
| expect(found.status).toBe('session'); | ||
| // DELETE | ||
| const delRes = await fetch(`http://localhost:${PORT}/api/keys/testprov`, { method: 'DELETE' }); | ||
| const delData = await delRes.json(); | ||
| expect(delRes.status).toBe(200); | ||
| expect(delData.deleted).toBe(true); | ||
| }); | ||
| it('POST /api/keys rejects missing fields', async () => { | ||
| const res = await fetch(`http://localhost:${PORT}/api/keys`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ provider: 'test' }), | ||
| }); | ||
| expect(res.status).toBe(400); | ||
| }); | ||
| it('POST /api/keys/test tests a provider', async () => { | ||
| const res = await fetch(`http://localhost:${PORT}/api/keys/test`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ provider: 'ollama' }), | ||
| }); | ||
| const data = await res.json(); | ||
| expect(res.status).toBe(200); | ||
| expect(data.provider).toBe('ollama'); | ||
| expect(data).toHaveProperty('available'); | ||
| expect(data).toHaveProperty('latencyMs'); | ||
| }); | ||
| it('POST /api/keys/test-all tests all providers', async () => { | ||
| const res = await fetch(`http://localhost:${PORT}/api/keys/test-all`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({}), | ||
| }); | ||
| const data = await res.json(); | ||
| expect(res.status).toBe(200); | ||
| expect(data.results).toBeDefined(); | ||
| expect(data.results.length).toBeGreaterThan(0); | ||
| expect(data.results[0]).toHaveProperty('provider'); | ||
| expect(data.results[0]).toHaveProperty('available'); | ||
| }); | ||
| it('returns 404 for unknown API routes', async () => { | ||
| const res = await fetch(`http://localhost:${PORT}/api/nonexistent`); | ||
| expect(res.status).toBe(404); | ||
| const data = await res.json(); | ||
| expect(data.error).toBe('Not found'); | ||
| }); | ||
@@ -61,10 +207,8 @@ it('includes CORS headers', async () => { | ||
| }); | ||
| it('/api/cost returns cost estimate', async () => { | ||
| const res = await fetch(`http://localhost:${PORT}/api/cost?model=gpt-4o&tokens=1000`); | ||
| const data = await res.json(); | ||
| it('serves index.html for non-API routes', async () => { | ||
| const res = await fetch(`http://localhost:${PORT}/`); | ||
| expect(res.status).toBe(200); | ||
| expect(data.estimate).toBeDefined(); | ||
| expect(data.estimate.cost).toBeGreaterThan(0); | ||
| expect(res.headers.get('content-type')).toContain('text/html'); | ||
| }); | ||
| }); | ||
| }); |
+2
-2
| { | ||
| "name": "agentkits", | ||
| "version": "2.0.0", | ||
| "description": "Multi-provider AI toolkit for agents. One interface, any model.", | ||
| "version": "2.0.1", | ||
| "description": "Agent Model Layer โ One-line LLM access with built-in memory. 29 providers, 18 embeddings, zero lock-in.", | ||
| "type": "module", | ||
@@ -6,0 +6,0 @@ "main": "dist/index.js", |
+134
-14
@@ -5,3 +5,3 @@ <div align="center"> | ||
| **ๅคๆจกๅ AI ๆบ่ฝไฝๅทฅๅ ทๅบ ยท Multi-provider AI toolkit for agents** | ||
| **Agent Model Layer โ ไธ่กไปฃ็ ๆฅๅ ฅ LLM๏ผ่ชๅธฆ่ฎฐๅฟ ยท One-line LLM access with built-in memory** | ||
@@ -11,6 +11,6 @@ [](https://www.npmjs.com/package/agentkits) | ||
| [](https://www.typescriptlang.org/) | ||
| [](#) | ||
| [](#) | ||
| [](https://nodejs.org/) | ||
| *19 ไธชๅคงๆจกๅ ยท 15 ไธชๅ้ๅๅผๆ ยท 40 ไธชๅ่ฝๆจกๅ ยท ้ถ้ๅฎ* | ||
| *29 ไธชๅคงๆจกๅ ยท 18 ไธชๅ้ๅๅผๆ ยท 40+ ๅ่ฝๆจกๅ ยท ้ถ้ๅฎ* | ||
@@ -25,17 +25,30 @@ [Quick Start](#ๅฟซ้ๅผๅง-quick-start) ยท [Providers](#ๅคงๆจกๅไพๅบๅ-llm-providers) ยท [Modules](#ๆจกๅ็ฎๅฝ-module-catalog) | ||
| ๆๅปบ AI ๆบ่ฝไฝไธๅบ่ขซ้ๅฎๅจๅไธไพๅบๅใAgentKits ๆไพ**็ปไธๆฅๅฃ**๏ผ่ฆ็ 19 ไธชๅคงๆจกๅๅ 15 ไธชๅ้ๅๅผๆใๅๆขๆจกๅๅช้ๆนไธไธช้ ็ฝฎ๏ผๆ ้้ๅไปฃ็ ใ | ||
| AgentKits ๆฏ The Self-Evolving Agent Stack ไธญ็ **Agent Model Layer**โโไธ่กไปฃ็ ๆฅๅ ฅไปปๆ LLM๏ผ่ชๅธฆ่ฎฐๅฟใ | ||
| > **๐จ๐ณ ๆทฑๅบฆๆฏๆไธญๅฝๅคงๆจกๅ็ๆ**๏ผ้ไนๅ้ฎใๆบ่ฐฑAIใๆไนๆ้ขใ้ถไธไธ็ฉใ็ก ๅบๆตๅจใ้ถ่ทๆ่พฐใ็พๅทๆบ่ฝใDeepSeekๆทฑๅบฆๆฑ็ดขใMiniMax โ ไธ็ญๅ ฌๆฐ๏ผไธๆฏ้ๅ ๅใ | ||
| ๅผๅ่ ไฝฟ็จ่ทฏๅพ๏ผโ ้ๆจกๆฟ (Agent Templates) โ โก **ๆฅๆจกๅ (Agent Model Layer)** โ โข ่ท่ตทๆฅ (Agent Runtime) โ โฃ ่ชๅจ่ฟๅ (Agent Memory)ใ | ||
| ๆๅปบ AI ๆบ่ฝไฝไธๅบ่ขซ้ๅฎๅจๅไธไพๅบๅใAgentKits ๆไพ**็ปไธๆฅๅฃ**๏ผ่ฆ็ 29 ไธชๅคงๆจกๅๅ 18 ไธชๅ้ๅๅผๆใๅๆขๆจกๅๅช้ๆนไธไธช้ ็ฝฎ๏ผๆ ้้ๅไปฃ็ ใ | ||
| **ๆ ธๅฟๅทฎๅผ๏ผ`withBrain()` ่ฎฐๅฟ้ๆใ** ๆฏๆฌก LLM ่ฐ็จ่ชๅจไธฒ่ DeepBrain ่ฎฐๅฟโโ่ฐ็จๅ `recall()` ๆฃ็ดข็ธๅ ณ็ฅ่ฏ๏ผ่ฐ็จๅ `learn()` ๅญๅจๆฐ็ป้ชใไฝ ็ Agent ไธๅๆฏๆ ็ถๆ็ API ่ฐ็จ๏ผ่ๆฏไธไธชๆ็ปญ่ช่ฟๅ็ๆบ่ฝไฝใ | ||
| **๐ ่ช่ฟๅ้ฃ่ฝฎ**๏ผTemplates ่ชๅธฆ Brain Seed โ Model Layer ๆฏๆฌก่ฐ็จ่ชๅจ learn โ Runtime ๆ็ปญ่ฟ่ก โ Memory ่ชๅจ evolve โ Agent ่ถๆฅ่ถๅผบใ | ||
| ```ts | ||
| import { createChat, createEmbedding } from 'agentkits'; | ||
| import { createChat } from 'agentkits'; | ||
| import { Brain, AgentBrain } from 'deepbrain'; | ||
| // ๅๆขไพๅบๅๅช้ๆนไธไธช่ฏ | ||
| const chat = createChat({ provider: 'deepseek' }); // ๆทฑๅบฆๆฑ็ดข | ||
| // ๆฎ้่ฐ็จ โ ๆ ็ถๆ๏ผ็จๅฎๅฐฑๅฟ | ||
| const chat = createChat({ provider: 'deepseek' }); | ||
| const reply = await chat.complete('่งฃ้้ๅญ่ฎก็ฎ'); | ||
| const emb = createEmbedding({ provider: 'dashscope' }); // ้ไนๅ้ฎ | ||
| const vector = await emb.embed('ไฝ ๅฅฝไธ็'); | ||
| // withBrain() โ ่ช่ฟๅ๏ผ่ถ็จ่ถ่ชๆ | ||
| const brain = new Brain({ database: './brain.db' }); | ||
| const agentBrain = new AgentBrain(brain, 'my-agent'); | ||
| const smartChat = chat.withBrain(agentBrain); | ||
| const smartReply = await smartChat.complete('่งฃ้้ๅญ่ฎก็ฎ'); | ||
| // โ ่ชๅจ recall ็ธๅ ณ่ฎฐๅฟ โ ็ๆๆดๅฅฝ็ๅ็ญ โ learn ่ฟๆฌกไบคไบ | ||
| ``` | ||
| > **๐จ๐ณ ๆทฑๅบฆๆฏๆไธญๅฝๅคงๆจกๅ็ๆ**๏ผ้ไนๅ้ฎใๆบ่ฐฑAIใๆไนๆ้ขใ้ถไธไธ็ฉใ็ก ๅบๆตๅจใ้ถ่ทๆ่พฐใ็พๅทๆบ่ฝใDeepSeekๆทฑๅบฆๆฑ็ดขใMiniMax โ ไธ็ญๅ ฌๆฐ๏ผไธๆฏ้ๅ ๅใ | ||
| ## ๅฟซ้ๅผๅง Quick Start | ||
@@ -217,4 +230,4 @@ | ||
| |------|---------|------| | ||
| | **ๅคงๆจกๅๅฏน่ฏ** | `agentkits/llm` | ็ปไธๅฏน่ฏ่กฅๅ จๆฅๅฃ๏ผๆฏๆ 19 ไธชไพๅบๅ | | ||
| | **ๅ้ๅ** | `agentkits/embedding` | ็ปไธๅ้ๅๆฅๅฃ๏ผๆฏๆ 15 ไธชๅผๆ | | ||
| | **ๅคงๆจกๅๅฏน่ฏ** | `agentkits/llm` | ็ปไธๅฏน่ฏ่กฅๅ จๆฅๅฃ๏ผๆฏๆ 29 ไธชไพๅบๅ | | ||
| | **ๅ้ๅ** | `agentkits/embedding` | ็ปไธๅ้ๅๆฅๅฃ๏ผๆฏๆ 18 ไธชๅผๆ | | ||
| | **ๆตๅผ่พๅบ** | `agentkits/streaming` | SSE ่งฃๆใๆต็ปๅใไธญๆญๆงๅถ | | ||
@@ -301,2 +314,75 @@ | **็ปๆๅ่พๅบ** | `agentkits/structured` | JSON Schema ๆ ก้ช็ LLM ่พๅบ | | ||
| ## ๐ ๅ้ญ็ฏ็ฅ่ฏ็ณป็ป (Dual-Loop Knowledge) | ||
| AgentKits ้่ฟ `withBrain()` ่ชๅจๅไธๅ้ญ็ฏ็ฅ่ฏ็ณป็ป๏ผ | ||
| ``` | ||
| โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | ||
| โ ๅฐ้ญ็ฏ๏ผๆฌๅฐ๏ผๅ ่ดน๏ผ โ | ||
| โ Agent ๆฌๅฐ learn โ recall โ evolve โ | ||
| โ ็ฆป็บฟไน่ฝ็จ๏ผๆฐๆฎๅฎๅ จๅจไฝ ๆ้ โ | ||
| โ โ | ||
| โ ๅคง้ญ็ฏ๏ผHub๏ผๅขๅผ๏ผ โ | ||
| โ Agent โ Workstation Hub ็ฅ่ฏๅ ฑไบซ โ | ||
| โ ้ไฝๆบๆ ง > ไธชไฝ็ป้ช๏ผๆฐ Agent ็ซๅจๅไบบ่ฉ่ไธ โ | ||
| โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | ||
| ``` | ||
| **ๆฌๅฐๆฏไธปไบบ๏ผHub ๆฏๅฉๆ**โโๆฒกๆ็ฝ็ปไน่ฝ็จ๏ผ่็ฝๅ่ชๅจๅๆญฅๅ่ฟๅใ | ||
| ``` | ||
| agentkits (Model Layer) โ ่ฐ LLM โ ไฝ ๅจ่ฟ้ | ||
| โ | ||
| opc-agent (Runtime) โ ่ท Agent๏ผๆฌๅฐ๏ผ | ||
| โ | ||
| deepbrain (Memory Engine) โ ๅญ็ฅ่ฏ๏ผๅผๆ๏ผ | ||
| โ | ||
| agent-workstation (Knowledge Platform) โ ็ฅ่ฏ็ๅฝๅจๆ๏ผHub๏ผ | ||
| ``` | ||
| --- | ||
| ## ๐ ็ซๅๅฏนๆฏ / Comparison | ||
| โ = ๆฏๆ๏ผ๐ถ = ้จๅๆฏๆ/้้ขๅค้ ็ฝฎ๏ผโ = ไธๆฏๆ | ||
| | ๅ่ฝ Feature | AgentKits | LiteLLM | Vercel AI SDK | OpenRouter | LangChain (LLM ๅฑ) | | ||
| |---|:-:|:-:|:-:|:-:|:-:| | ||
| | **ๅฎไฝ** | Agent Model Layer๏ผไธ่กไปฃ็ ๆฅ LLM + ่ชๅธฆ่ฎฐๅฟ๏ผ | LLM ็ปไธ็ฝๅ ณ/ไปฃ็ | AI ๅบ็จ SDK (React) | API ่ๅๅธๅบ | AI ๅ จๆ ๆกๆถ | | ||
| | **่ฏญ่จ** | TypeScript | Python | TypeScript | REST API | Python (TS ๆ้) | | ||
| | **LLM Provider ๆฐ** | **29** | **100+** | 20+ | **500+ / 60 ๆไพๅ** | 50+ | | ||
| | **Embedding Provider ๆฐ** | **18** | ๐ถ ไธปๆต | ๐ถ ๅ ไธช | โ | ๐ถ ไธปๆต | | ||
| | **ไธญๅฝๆจกๅๆทฑๅบฆๆฏๆ** | โ 9 ๅฎถไธ็ญๅ ฌๆฐ | ๐ถ ๅ ผๅฎน API | โ ๆ้ | ๐ถ ้จๅ | ๐ถ ๅ ผๅฎน API | | ||
| | **่ฎฐๅฟๅขๅผบ (withBrain)** | โ ่ชๅจ recall + learn | โ | โ | โ | ๐ถ Memory ๆจกๅ | | ||
| | **Function Calling** | โ ่ทจไพๅบๅ็ปไธ | โ | โ ็ฑปๅๅฎๅ จ | ๐ถ ๅๅณไบๆจกๅ | โ | | ||
| | **Streaming** | โ SSE+ๆต็ปๅ+ไธญๆญ | โ | โ | โ | โ | | ||
| | **็ปๆๅ่พๅบ** | โ JSON Schema | ๐ถ ้ไผ | โ Zod | ๐ถ ้ไผ | โ | | ||
| | **Agent ๅพช็ฏ (ReAct)** | โ ๅ ็ฝฎ | โ ็บฏ่ทฏ็ฑ | ๐ถ SDK 6 Agent | โ ็บฏ่ทฏ็ฑ | โ LangGraph | | ||
| | **ๅทฅไฝๆตๅผๆ** | โ ๅๆฏ+ๅนถ่ก | โ | โ | โ | โ LangGraph | | ||
| | **RAG Pipeline** | โ ๅฎๆด+้ๆๅบ | โ | โ | โ | โ | | ||
| | **MCP Client** | โ | โ | ๐ถ | โ | ๐ถ | | ||
| | **A2A ๅ่ฎฎ** | โ Google A2A | โ | โ | โ | โ | | ||
| | **Cost ่ฎก็ฎ** | โ ่ทจไพๅบๅๅฏนๆฏ | โ ๅ ็ฝฎ | ๐ถ token ็ๆง | โ ่ดฆๅ็ปไธ | โ | | ||
| | **Token ่ฎกๆฐ** | โ ๆๆจกๅ็ฒพ็กฎ | ๐ถ | ๐ถ | ๐ถ | ๐ถ | | ||
| | **้่ฏฏ้่ฏ** | โ ๆๆฐ้้ฟ+ๆๅจ | โ ่ชๅจๆ ้่ฝฌ็งป | โ | โ ่ชๅจ fallback | ๐ถ | | ||
| | **ๆบ่ฝ่ทฏ็ฑ** | โ ๆๆฌ/้ๅบฆ่ทฏ็ฑ | โ ่ด่ฝฝๅ่กก | โ | โ ๆบ่ฝ่ทฏ็ฑ | โ | | ||
| | **็ผๅญ** | โ LRU + TTL | โ ่ฏญไน็ผๅญ | โ | โ | ๐ถ | | ||
| | **้ๆต** | โ Token Bucket | โ | โ | โ | โ | | ||
| | **Vision** | โ ่ทจไพๅบๅ | โ | ๐ถ | ๐ถ | ๐ถ | | ||
| | **TTS / STT** | โ 3 ๅฎถ | โ ๆ ๅ็ซฏ็น | โ | โ | โ | | ||
| | **ๅพๅ็ๆ** | โ DALL-E ็ญ | โ | ๐ถ | โ | โ | | ||
| | **ไปฃ็ ่งฃ้ๅจ** | โ ๆฒ็ฎฑ JS/Py/Shell | โ | โ | โ | ๐ถ | | ||
| | **ๅฎๅ จๆคๆ ** | โ PII+ๅ ๅฎน่ฟๆปค | โ ๅ ณ้ฎ่ฏ+ๆญฃๅ | โ | โ | ๐ถ | | ||
| | **ๅฏ่งๆตๆง (OTel)** | โ ๅๅธๅผ่ฟฝ่ธช | โ ๅคๅนณๅฐ | ๐ถ | โ | ๐ถ LangSmith | | ||
| | **Benchmark** | โ ๅปถ่ฟ+ๅๅ้ | โ | โ | โ | โ | | ||
| | **OpenAI ๅ ผๅฎนไปฃ็** | โ `agentkits serve` | โ ๆ ธๅฟๅ่ฝ | โ | โ | โ | | ||
| | **TypeScript ๅ็** | โ | โ Python | โ | REST API | โ Python | | ||
| | **่ฎธๅฏ่ฏ** | Apache-2.0 | Apache-2.0 | Apache-2.0 | ไธๆ (API ๆๅก) | MIT | | ||
| **AgentKits ็ฌๆไผๅฟ**๏ผTypeScript ๅ็ + ไธญๅฝๆจกๅไธ็ญๅ ฌๆฐ (9 ๅฎถ) + **withBrain() ่ช่ฟๅ่ฎฐๅฟ**๏ผๆฏๆฌก LLM ่ฐ็จ่ชๅจ recall + learn๏ผAgent ่ถ็จ่ถ่ชๆ๏ผ + Agent + RAG + Workflow + A2A ไธไฝๅใไฝไธบ The Self-Evolving Agent Stack ็ Agent Model Layer๏ผไธ่กไปฃ็ ๆฅๅ ฅ LLM๏ผ่ชๅธฆ่ฎฐๅฟใ | ||
| > ๅฏนๆฏๅบไบๅ้กน็ฎๅ ฌๅผๆๆกฃ๏ผๆช่ณ 2026 ๅนด 4 ๆ๏ผ๏ผๅฆๆๅๅทฎๆฌข่ฟ [Issue ๆๆญฃ](https://github.com/Deepleaper/agentkits/issues)ใ | ||
| --- | ||
| ## ๆถๆ Architecture | ||
@@ -364,4 +450,10 @@ | ||
| AgentKits is the **Agent Model Layer** in The Self-Evolving Agent Stack โ one-line LLM access with built-in memory. | ||
| Developer path: โ Pick a template (Agent Templates) โ โก **Connect models (Agent Model Layer)** โ โข Run it (Agent Runtime) โ โฃ Auto-evolve (Agent Memory). | ||
| AgentKits provides a **unified TypeScript interface** across 29 LLM providers and 18 embedding providers. Switching models requires changing one config value โ no code rewrite needed. | ||
| **Core differentiator: `withBrain()` memory integration.** Every LLM call automatically connects to DeepBrain memory โ `recall()` before the call to retrieve relevant knowledge, `learn()` after to store new experience. Your Agent is no longer a stateless API call, but a continuously self-evolving intelligence. | ||
| **First-class support for Chinese LLM ecosystem**: DashScope (Qwen), Zhipu AI (GLM), Moonshot (Kimi), Yi, Baichuan, SiliconFlow, StepFun, DeepSeek, MiniMax. | ||
@@ -391,4 +483,4 @@ | ||
| - **LLM (19)**: OpenAI, Gemini, DeepSeek, DashScope, Zhipu, Moonshot, Yi, Baichuan, SiliconFlow, StepFun, MiniMax, Grok, Cohere, Fireworks, Together, Groq, Perplexity, Ollama, Custom | ||
| - **Embedding (15)**: OpenAI, Gemini, DashScope, DeepSeek, Zhipu, SiliconFlow, Cohere, Jina, Voyage, Mixedbread, Nomic, Fireworks, Together, Ollama, Custom | ||
| - **LLM (29)**: OpenAI, Gemini, DeepSeek, DashScope, Zhipu, Moonshot, Yi, Baichuan, SiliconFlow, StepFun, MiniMax, Grok, Cohere, Fireworks, Together, Groq, Perplexity, Ollama, Custom | ||
| - **Embedding (18)**: OpenAI, Gemini, DashScope, DeepSeek, Zhipu, SiliconFlow, Cohere, Jina, Voyage, Mixedbread, Nomic, Fireworks, Together, Ollama, Custom | ||
@@ -412,2 +504,30 @@ ## Module Catalog (40 modules) | ||
| ## ๐ Dual-Loop Knowledge System | ||
| AgentKits participates in the dual-loop knowledge system via `withBrain()`: | ||
| ``` | ||
| โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | ||
| โ Small Loop (Local, Free) โ | ||
| โ Agent local learn โ recall โ evolve โ | ||
| โ Works offline, data stays on your machine โ | ||
| โ โ | ||
| โ Big Loop (Hub, Value-Add) โ | ||
| โ Agent โ Workstation Hub knowledge sharing โ | ||
| โ Collective wisdom > individual experience โ | ||
| โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | ||
| ``` | ||
| **Local is the owner, Hub is the helper** โ works without internet, auto-syncs when connected. | ||
| ``` | ||
| agentkits (Model Layer) โ LLM calls โ You are here | ||
| โ | ||
| opc-agent (Runtime) โ run Agents (local) | ||
| โ | ||
| deepbrain (Memory Engine) โ store knowledge (engine) | ||
| โ | ||
| agent-workstation (Knowledge Platform) โ knowledge lifecycle (Hub) | ||
| ``` | ||
| ## License | ||
@@ -414,0 +534,0 @@ |
+675
-270
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <html lang="zh-CN"> | ||
| <head> | ||
| <meta charset="UTF-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| <title>AgentKits Dashboard</title> | ||
| <title>AgentKits ๆจกๅ็ฎก็</title> | ||
| <style> | ||
@@ -19,2 +19,3 @@ :root { | ||
| --accent-hover: #818cf8; | ||
| --accent-dim: rgba(99,102,241,.12); | ||
| --green: #22c55e; | ||
@@ -26,79 +27,144 @@ --red: #ef4444; | ||
| --font: 'Segoe UI', system-ui, -apple-system, sans-serif; | ||
| --mono: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', monospace; | ||
| --radius: 8px; | ||
| --mono: 'Cascadia Code', 'Fira Code', monospace; | ||
| --radius: 10px; | ||
| --radius-lg: 16px; | ||
| } | ||
| * { margin:0; padding:0; box-sizing:border-box; } | ||
| body { font-family: var(--font); background: var(--bg-primary); color: var(--text-primary); min-height:100vh; } | ||
| a { color: var(--accent); text-decoration:none; } | ||
| *{margin:0;padding:0;box-sizing:border-box} | ||
| body{font-family:var(--font);background:var(--bg-primary);color:var(--text-primary);min-height:100vh;overflow-x:hidden} | ||
| a{color:var(--accent);text-decoration:none} | ||
| /* Layout */ | ||
| .app { display:flex; min-height:100vh; } | ||
| .sidebar { width:220px; background:var(--bg-secondary); border-right:1px solid var(--border); padding:16px 0; flex-shrink:0; position:fixed; height:100vh; overflow-y:auto; } | ||
| .sidebar h1 { font-size:18px; padding:0 16px 16px; border-bottom:1px solid var(--border); margin-bottom:8px; } | ||
| .sidebar h1 span { color:var(--accent); } | ||
| .nav-item { display:block; padding:10px 16px; color:var(--text-secondary); cursor:pointer; transition:all .15s; font-size:14px; border-left:3px solid transparent; } | ||
| .nav-item:hover { background:var(--bg-tertiary); color:var(--text-primary); } | ||
| .nav-item.active { color:var(--accent); border-left-color:var(--accent); background:rgba(99,102,241,.08); } | ||
| .main { margin-left:220px; flex:1; padding:24px; max-width:1200px; } | ||
| /* โโ Layout โโ */ | ||
| .app{display:flex;min-height:100vh} | ||
| .sidebar{width:240px;background:var(--bg-secondary);border-right:1px solid var(--border);padding:20px 0;flex-shrink:0;position:fixed;height:100vh;overflow-y:auto;z-index:100} | ||
| .sidebar .logo{padding:0 20px 20px;border-bottom:1px solid var(--border);margin-bottom:12px;display:flex;align-items:center;gap:10px} | ||
| .sidebar .logo h1{font-size:20px;font-weight:700} | ||
| .sidebar .logo h1 span{color:var(--accent)} | ||
| .sidebar .logo .ver{font-size:11px;color:var(--text-muted);background:var(--bg-tertiary);padding:2px 6px;border-radius:4px} | ||
| .nav-section{padding:8px 16px 4px;font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:1px} | ||
| .nav-item{display:flex;align-items:center;gap:10px;padding:11px 20px;color:var(--text-secondary);cursor:pointer;transition:all .15s;font-size:14px;border-left:3px solid transparent} | ||
| .nav-item:hover{background:var(--bg-tertiary);color:var(--text-primary)} | ||
| .nav-item.active{color:var(--accent);border-left-color:var(--accent);background:var(--accent-dim)} | ||
| .nav-item .icon{font-size:18px;width:24px;text-align:center} | ||
| .main{margin-left:240px;flex:1;padding:28px;max-width:1100px} | ||
| /* Pages */ | ||
| .page { display:none; } | ||
| .page.active { display:block; } | ||
| .page h2 { font-size:22px; margin-bottom:16px; } | ||
| /* โโ Pages โโ */ | ||
| .page{display:none;animation:fadeIn .25s ease} | ||
| .page.active{display:block} | ||
| @keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}} | ||
| .page-header{margin-bottom:24px} | ||
| .page-header h2{font-size:24px;font-weight:700} | ||
| .page-header p{color:var(--text-secondary);margin-top:4px;font-size:14px} | ||
| /* Cards */ | ||
| .cards { display:grid; grid-template-columns:repeat(auto-fit,minmax(200px,1fr)); gap:16px; margin-bottom:24px; } | ||
| .card { background:var(--bg-card); border:1px solid var(--border); border-radius:var(--radius); padding:16px; } | ||
| .card .label { font-size:12px; color:var(--text-muted); text-transform:uppercase; letter-spacing:.5px; } | ||
| .card .value { font-size:28px; font-weight:700; margin-top:4px; } | ||
| .card .sub { font-size:12px; color:var(--text-secondary); margin-top:4px; } | ||
| /* โโ Cards โโ */ | ||
| .cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;margin-bottom:24px} | ||
| .card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:20px;transition:border-color .15s} | ||
| .card:hover{border-color:var(--accent)} | ||
| .card .label{font-size:12px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px} | ||
| .card .value{font-size:30px;font-weight:700} | ||
| .card .sub{font-size:12px;color:var(--text-secondary);margin-top:6px} | ||
| /* Tables */ | ||
| table { width:100%; border-collapse:collapse; background:var(--bg-card); border:1px solid var(--border); border-radius:var(--radius); overflow:hidden; } | ||
| th, td { padding:10px 14px; text-align:left; border-bottom:1px solid var(--border); font-size:13px; } | ||
| th { background:var(--bg-tertiary); color:var(--text-secondary); font-weight:600; cursor:pointer; user-select:none; font-size:12px; text-transform:uppercase; letter-spacing:.5px; } | ||
| th:hover { color:var(--text-primary); } | ||
| td { color:var(--text-primary); } | ||
| tr:last-child td { border-bottom:none; } | ||
| tr:hover td { background:rgba(99,102,241,.04); } | ||
| /* โโ Provider Cards โโ */ | ||
| .provider-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:16px} | ||
| .provider-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:20px;cursor:pointer;transition:all .2s} | ||
| .provider-card:hover{border-color:var(--accent);transform:translateY(-2px);box-shadow:0 8px 24px rgba(0,0,0,.3)} | ||
| .provider-card .pc-head{display:flex;align-items:center;gap:12px;margin-bottom:12px} | ||
| .provider-card .pc-emoji{font-size:32px} | ||
| .provider-card .pc-name{font-size:16px;font-weight:600} | ||
| .provider-card .pc-label{font-size:12px;color:var(--text-secondary)} | ||
| .provider-card .pc-stats{display:flex;gap:16px;margin-bottom:10px} | ||
| .provider-card .pc-stat{font-size:12px;color:var(--text-secondary)} | ||
| .provider-card .pc-stat strong{color:var(--text-primary);font-size:14px} | ||
| .provider-card .pc-desc{font-size:12px;color:var(--text-muted);line-height:1.5} | ||
| .category-label{font-size:15px;font-weight:600;color:var(--text-secondary);margin:24px 0 12px;padding-bottom:8px;border-bottom:1px solid var(--border)} | ||
| .category-label:first-child{margin-top:0} | ||
| /* Forms */ | ||
| .form-group { margin-bottom:16px; } | ||
| .form-group label { display:block; font-size:13px; color:var(--text-secondary); margin-bottom:6px; } | ||
| select, input, textarea { background:var(--bg-tertiary); border:1px solid var(--border); color:var(--text-primary); padding:8px 12px; border-radius:var(--radius); font-size:14px; width:100%; font-family:var(--font); } | ||
| select:focus, input:focus, textarea:focus { outline:none; border-color:var(--accent); } | ||
| textarea { font-family:var(--mono); min-height:120px; resize:vertical; } | ||
| button { background:var(--accent); color:#fff; border:none; padding:10px 20px; border-radius:var(--radius); cursor:pointer; font-size:14px; font-weight:600; transition:background .15s; } | ||
| button:hover { background:var(--accent-hover); } | ||
| button:disabled { opacity:.5; cursor:not-allowed; } | ||
| /* โโ Model List โโ */ | ||
| .model-list{display:flex;flex-direction:column;gap:12px} | ||
| .model-item{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:16px;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap} | ||
| .model-item .mi-info{flex:1;min-width:200px} | ||
| .model-item .mi-name{font-size:15px;font-weight:600} | ||
| .model-item .mi-meta{font-size:12px;color:var(--text-secondary);margin-top:4px;display:flex;gap:12px;flex-wrap:wrap} | ||
| .model-item .mi-caps{display:flex;gap:6px;margin-top:6px;flex-wrap:wrap} | ||
| /* Status badges */ | ||
| .badge { display:inline-block; padding:3px 8px; border-radius:12px; font-size:11px; font-weight:600; } | ||
| .badge-green { background:rgba(34,197,94,.15); color:var(--green); } | ||
| .badge-red { background:rgba(239,68,68,.15); color:var(--red); } | ||
| .badge-yellow { background:rgba(234,179,8,.15); color:var(--yellow); } | ||
| /* โโ Badges โโ */ | ||
| .badge{display:inline-block;padding:3px 8px;border-radius:12px;font-size:11px;font-weight:600} | ||
| .badge-green{background:rgba(34,197,94,.15);color:var(--green)} | ||
| .badge-red{background:rgba(239,68,68,.15);color:var(--red)} | ||
| .badge-yellow{background:rgba(234,179,8,.15);color:var(--yellow)} | ||
| .badge-blue{background:rgba(59,130,246,.15);color:var(--blue)} | ||
| .badge-purple{background:rgba(99,102,241,.15);color:var(--accent)} | ||
| .badge-orange{background:rgba(249,115,22,.15);color:var(--orange)} | ||
| .cap-badge{font-size:10px;padding:2px 6px;border-radius:4px;background:var(--bg-tertiary);color:var(--text-secondary);border:1px solid var(--border)} | ||
| /* Result boxes */ | ||
| .result-box { background:var(--bg-tertiary); border:1px solid var(--border); border-radius:var(--radius); padding:16px; margin-top:16px; font-family:var(--mono); font-size:13px; white-space:pre-wrap; max-height:400px; overflow-y:auto; } | ||
| /* โโ Tables โโ */ | ||
| table{width:100%;border-collapse:collapse;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden} | ||
| th,td{padding:10px 14px;text-align:left;border-bottom:1px solid var(--border);font-size:13px} | ||
| th{background:var(--bg-tertiary);color:var(--text-secondary);font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.5px} | ||
| td{color:var(--text-primary)} | ||
| tr:last-child td{border-bottom:none} | ||
| tr:hover td{background:rgba(99,102,241,.04)} | ||
| /* Health grid */ | ||
| .health-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); gap:16px; } | ||
| .health-card { background:var(--bg-card); border:1px solid var(--border); border-radius:var(--radius); padding:16px; } | ||
| .health-card h3 { font-size:15px; margin-bottom:8px; } | ||
| .health-card .status { font-size:13px; } | ||
| /* โโ Forms โโ */ | ||
| .form-group{margin-bottom:16px} | ||
| .form-group label{display:block;font-size:13px;color:var(--text-secondary);margin-bottom:6px} | ||
| select,input{background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-primary);padding:10px 14px;border-radius:var(--radius);font-size:14px;width:100%;font-family:var(--font)} | ||
| select:focus,input:focus{outline:none;border-color:var(--accent)} | ||
| .btn{display:inline-flex;align-items:center;gap:6px;background:var(--accent);color:#fff;border:none;padding:10px 20px;border-radius:var(--radius);cursor:pointer;font-size:14px;font-weight:600;transition:all .15s} | ||
| .btn:hover{background:var(--accent-hover);transform:translateY(-1px)} | ||
| .btn:disabled{opacity:.5;cursor:not-allowed;transform:none} | ||
| .btn-outline{background:transparent;border:1px solid var(--border);color:var(--text-secondary)} | ||
| .btn-outline:hover{border-color:var(--accent);color:var(--accent);background:var(--accent-dim)} | ||
| .btn-danger{background:var(--red)} | ||
| .btn-danger:hover{background:#dc2626} | ||
| .btn-sm{padding:6px 12px;font-size:12px} | ||
| .inline-form{display:flex;gap:12px;align-items:end;flex-wrap:wrap} | ||
| .inline-form .form-group{margin-bottom:0} | ||
| /* Inline form */ | ||
| .inline-form { display:flex; gap:12px; align-items:end; flex-wrap:wrap; } | ||
| .inline-form .form-group { margin-bottom:0; } | ||
| /* โโ Result/Status โโ */ | ||
| .result-box{background:var(--bg-tertiary);border:1px solid var(--border);border-radius:var(--radius);padding:16px;margin-top:16px;font-family:var(--mono);font-size:13px;white-space:pre-wrap;max-height:400px;overflow-y:auto} | ||
| .spinner{display:inline-block;width:16px;height:16px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .6s linear infinite;vertical-align:middle} | ||
| @keyframes spin{to{transform:rotate(360deg)}} | ||
| /* Provider tag */ | ||
| .provider-tag { font-size:11px; padding:2px 6px; border-radius:4px; background:var(--bg-tertiary); color:var(--text-secondary); } | ||
| /* โโ Health Grid โโ */ | ||
| .health-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px} | ||
| .health-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:16px;display:flex;align-items:center;gap:12px} | ||
| .health-card .hc-emoji{font-size:28px} | ||
| .health-card .hc-info{flex:1} | ||
| .health-card .hc-name{font-size:14px;font-weight:600} | ||
| .health-card .hc-status{font-size:12px;color:var(--text-secondary);margin-top:2px} | ||
| /* Loading */ | ||
| .spinner { display:inline-block; width:16px; height:16px; border:2px solid var(--border); border-top-color:var(--accent); border-radius:50%; animation:spin .6s linear infinite; } | ||
| @keyframes spin { to { transform:rotate(360deg); } } | ||
| /* โโ Key List โโ */ | ||
| .key-item{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:16px;display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px} | ||
| .key-item .ki-info{display:flex;align-items:center;gap:12px} | ||
| .key-item .ki-provider{font-weight:600;min-width:100px} | ||
| .key-item .ki-masked{font-family:var(--mono);font-size:13px;color:var(--text-secondary)} | ||
| .key-item .ki-actions{display:flex;gap:8px} | ||
| @media(max-width:768px) { | ||
| .sidebar { display:none; } | ||
| .main { margin-left:0; } | ||
| /* โโ SVG Chart โโ */ | ||
| .chart-container{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:20px;margin-bottom:24px} | ||
| .chart-container h3{font-size:15px;margin-bottom:16px} | ||
| /* โโ Budget Bar โโ */ | ||
| .budget-bar{height:8px;background:var(--bg-tertiary);border-radius:4px;overflow:hidden;margin-top:8px} | ||
| .budget-fill{height:100%;border-radius:4px;transition:width .5s ease} | ||
| /* โโ Back Button โโ */ | ||
| .back-link{display:inline-flex;align-items:center;gap:6px;color:var(--text-secondary);font-size:13px;margin-bottom:16px;cursor:pointer} | ||
| .back-link:hover{color:var(--accent)} | ||
| /* โโ Mobile โโ */ | ||
| .mobile-nav{display:none;position:fixed;bottom:0;left:0;right:0;background:var(--bg-secondary);border-top:1px solid var(--border);z-index:100;padding:6px 0} | ||
| .mobile-nav .mn-items{display:flex;justify-content:space-around} | ||
| .mobile-nav .mn-item{display:flex;flex-direction:column;align-items:center;gap:2px;padding:6px 12px;font-size:10px;color:var(--text-muted);cursor:pointer} | ||
| .mobile-nav .mn-item.active{color:var(--accent)} | ||
| .mobile-nav .mn-item .icon{font-size:20px} | ||
| @media(max-width:768px){ | ||
| .sidebar{display:none} | ||
| .main{margin-left:0;padding:16px 16px 80px} | ||
| .mobile-nav{display:block} | ||
| .cards{grid-template-columns:repeat(2,1fr);gap:10px} | ||
| .provider-grid{grid-template-columns:1fr} | ||
| .health-grid{grid-template-columns:1fr} | ||
| .model-item{flex-direction:column;align-items:flex-start} | ||
| } | ||
@@ -109,106 +175,126 @@ </style> | ||
| <div class="app"> | ||
| <!-- Sidebar --> | ||
| <nav class="sidebar"> | ||
| <h1>โก <span>AgentKits</span></h1> | ||
| <div class="nav-item active" data-page="dashboard">๐ Dashboard</div> | ||
| <div class="nav-item" data-page="models">๐ค Models</div> | ||
| <div class="nav-item" data-page="recommend">๐ฏ Recommend</div> | ||
| <div class="nav-item" data-page="cost">๐ฐ Cost Calculator</div> | ||
| <div class="nav-item" data-page="health">๐ฅ Health Check</div> | ||
| <div class="nav-item" data-page="playground">๐งช Playground</div> | ||
| <div class="logo"> | ||
| <h1>โก <span>AgentKits</span></h1> | ||
| <span class="ver">v2.0</span> | ||
| </div> | ||
| <div class="nav-section">ๆฆ่ง Overview</div> | ||
| <div class="nav-item active" data-page="dashboard"><span class="icon">๐</span>้ฆ้กต Dashboard</div> | ||
| <div class="nav-section">ๆจกๅ Models</div> | ||
| <div class="nav-item" data-page="models"><span class="icon">๐ช</span>ๆจกๅๅธๅบ Market</div> | ||
| <div class="nav-section">็ฎก็ Manage</div> | ||
| <div class="nav-item" data-page="keys"><span class="icon">๐</span>API Key ็ฎก็</div> | ||
| <div class="nav-item" data-page="cost"><span class="icon">๐ฐ</span>่ดน็จ็ๆฟ Cost</div> | ||
| <div class="nav-item" data-page="test"><span class="icon">๐</span>่ฟๆฅๆต่ฏ Test</div> | ||
| </nav> | ||
| <!-- Mobile Nav --> | ||
| <div class="mobile-nav"> | ||
| <div class="mn-items"> | ||
| <div class="mn-item active" data-page="dashboard"><span class="icon">๐</span>้ฆ้กต</div> | ||
| <div class="mn-item" data-page="models"><span class="icon">๐ช</span>ๅธๅบ</div> | ||
| <div class="mn-item" data-page="keys"><span class="icon">๐</span>Keys</div> | ||
| <div class="mn-item" data-page="cost"><span class="icon">๐ฐ</span>่ดน็จ</div> | ||
| <div class="mn-item" data-page="test"><span class="icon">๐</span>ๆต่ฏ</div> | ||
| </div> | ||
| </div> | ||
| <div class="main"> | ||
| <!-- Dashboard --> | ||
| <!-- โโโโโโโ Dashboard โโโโโโโ --> | ||
| <div id="dashboard" class="page active"> | ||
| <h2>๐ Dashboard</h2> | ||
| <div class="page-header"> | ||
| <h2>๐ ๆจกๅ็ฎก็้ขๆฟ</h2> | ||
| <p>ๅฝๅๆจกๅ็ถๆๆฆ่ง / Model status overview</p> | ||
| </div> | ||
| <div class="cards" id="dash-cards"></div> | ||
| <h3 style="margin-bottom:12px">Provider Overview</h3> | ||
| <div id="dash-active" style="margin-bottom:24px"></div> | ||
| <h3 style="margin-bottom:12px;font-size:15px;color:var(--text-secondary)">ๆไพๅๆฆ่ง / Provider Overview</h3> | ||
| <div id="dash-providers" class="health-grid"></div> | ||
| </div> | ||
| <!-- Models --> | ||
| <!-- โโโโโโโ Models Market โโโโโโโ --> | ||
| <div id="models" class="page"> | ||
| <h2>๐ค Models</h2> | ||
| <div style="margin-bottom:12px"><input id="model-search" placeholder="Search models..." style="max-width:300px"></div> | ||
| <table id="models-table"> | ||
| <thead><tr> | ||
| <th data-sort="name">Model</th> | ||
| <th data-sort="provider">Provider</th> | ||
| <th data-sort="inputPricePer1M">Input $/1M</th> | ||
| <th data-sort="outputPricePer1M">Output $/1M</th> | ||
| <th data-sort="contextWindow">Context</th> | ||
| <th>Best For</th> | ||
| </tr></thead> | ||
| <tbody></tbody> | ||
| </table> | ||
| <div class="page-header"> | ||
| <h2>๐ช ๆจกๅๅธๅบ</h2> | ||
| <p>ๆต่งๆๆๆฏๆ็ๆจกๅๆไพๅๅๆจกๅ / Browse all supported providers & models</p> | ||
| </div> | ||
| <div id="models-content"></div> | ||
| </div> | ||
| <!-- Recommend --> | ||
| <div id="recommend" class="page"> | ||
| <h2>๐ฏ Model Recommender</h2> | ||
| <div class="inline-form"> | ||
| <div class="form-group"> | ||
| <label>Task Type</label> | ||
| <select id="rec-task"> | ||
| <option value="chat">Chat</option> | ||
| <option value="coding">Coding</option> | ||
| <option value="analysis">Analysis</option> | ||
| <option value="embedding">Embedding</option> | ||
| <option value="vision">Vision</option> | ||
| </select> | ||
| <!-- โโโโโโโ Provider Detail โโโโโโโ --> | ||
| <div id="provider-detail" class="page"> | ||
| <div id="provider-detail-content"></div> | ||
| </div> | ||
| <!-- โโโโโโโ Keys โโโโโโโ --> | ||
| <div id="keys" class="page"> | ||
| <div class="page-header"> | ||
| <h2>๐ API Key ็ฎก็</h2> | ||
| <p>็ฎก็ไฝ ็ API ๅฏ้ฅ๏ผๆต่ฏ่ฟๆฅ็ถๆ / Manage API keys & test connectivity</p> | ||
| </div> | ||
| <div id="keys-list" style="margin-bottom:24px"></div> | ||
| <div style="background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:20px"> | ||
| <h3 style="font-size:15px;margin-bottom:16px">โ ๆทปๅ ๆฐ Key / Add New Key</h3> | ||
| <div class="inline-form"> | ||
| <div class="form-group"> | ||
| <label>ๆไพๅ Provider</label> | ||
| <select id="key-provider" style="width:200px"></select> | ||
| </div> | ||
| <div class="form-group" style="flex:1"> | ||
| <label>API Key</label> | ||
| <input id="key-value" type="password" placeholder="sk-xxx..."> | ||
| </div> | ||
| <div class="form-group"> | ||
| <button class="btn" onclick="addKey()">ๆทปๅ Add</button> | ||
| </div> | ||
| <div class="form-group"> | ||
| <button class="btn btn-outline" onclick="testAllKeys()">๐ ๆต่ฏๅ จ้จ</button> | ||
| </div> | ||
| </div> | ||
| <div class="form-group"> | ||
| <label>Budget</label> | ||
| <select id="rec-budget"> | ||
| <option value="free">Free</option> | ||
| <option value="low">Low</option> | ||
| <option value="medium" selected>Medium</option> | ||
| <option value="high">High</option> | ||
| </select> | ||
| </div> | ||
| <div class="form-group"><button onclick="doRecommend()">Get Recommendations</button></div> | ||
| </div> | ||
| <div id="rec-results"></div> | ||
| </div> | ||
| <!-- Cost Calculator --> | ||
| <!-- โโโโโโโ Cost โโโโโโโ --> | ||
| <div id="cost" class="page"> | ||
| <h2>๐ฐ Cost Calculator</h2> | ||
| <div class="inline-form"> | ||
| <div class="form-group"> | ||
| <label>Model</label> | ||
| <select id="cost-model"></select> | ||
| <div class="page-header"> | ||
| <h2>๐ฐ ่ดน็จ็ๆฟ</h2> | ||
| <p>ๆฅ็ Token ๆถ่ๅ่ดน็จ่ถๅฟ / View token usage & cost trends</p> | ||
| </div> | ||
| <div class="cards" id="cost-cards"></div> | ||
| <div id="cost-chart" class="chart-container"> | ||
| <h3>๐ ่ดน็จ่ถๅฟ / Cost Trend (30ๅคฉ)</h3> | ||
| <div id="trend-chart" style="margin-top:16px"></div> | ||
| </div> | ||
| <div style="margin-bottom:24px"> | ||
| <h3 style="font-size:15px;margin-bottom:12px">ๆๆจกๅๆถ่ / By Model</h3> | ||
| <table id="cost-by-model"> | ||
| <thead><tr><th>ๆจกๅ Model</th><th>Provider</th><th>Tokens</th><th>่ดน็จ Cost</th></tr></thead> | ||
| <tbody></tbody> | ||
| </table> | ||
| </div> | ||
| <div style="background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:20px"> | ||
| <h3 style="font-size:15px;margin-bottom:12px">๐ณ ้ข็ฎ่ฎพ็ฝฎ / Budget Setting</h3> | ||
| <div id="budget-display"></div> | ||
| <div class="inline-form" style="margin-top:12px"> | ||
| <div class="form-group"> | ||
| <label>ๆ้ข็ฎ (USD)</label> | ||
| <input id="budget-input" type="number" value="100" style="width:140px"> | ||
| </div> | ||
| <div class="form-group"> | ||
| <button class="btn btn-sm" onclick="setBudget()">ไฟๅญ Save</button> | ||
| </div> | ||
| </div> | ||
| <div class="form-group"> | ||
| <label>Tokens/Day</label> | ||
| <input id="cost-tokens" type="number" value="100000" style="width:140px"> | ||
| </div> | ||
| <div class="form-group"> | ||
| <label>Days</label> | ||
| <input id="cost-days" type="number" value="30" style="width:80px"> | ||
| </div> | ||
| <div class="form-group"><button onclick="calcCost()">Calculate</button></div> | ||
| </div> | ||
| <div id="cost-results"></div> | ||
| </div> | ||
| <!-- Health Check --> | ||
| <div id="health" class="page"> | ||
| <h2>๐ฅ Provider Health Check</h2> | ||
| <button onclick="checkAllProviders()" style="margin-bottom:16px">Check All Providers</button> | ||
| <div id="health-grid" class="health-grid"></div> | ||
| </div> | ||
| <!-- Playground --> | ||
| <div id="playground" class="page"> | ||
| <h2>๐งช Playground</h2> | ||
| <div class="form-group"> | ||
| <label>Model</label> | ||
| <select id="play-model"></select> | ||
| <!-- โโโโโโโ Test โโโโโโโ --> | ||
| <div id="test" class="page"> | ||
| <div class="page-header"> | ||
| <h2>๐ ่ฟๆฅๆต่ฏ</h2> | ||
| <p>ๆต่ฏๆๆๅทฒ้ ็ฝฎๆจกๅ็่ฟ้ๆง / Test connectivity for all configured models</p> | ||
| </div> | ||
| <div class="form-group"> | ||
| <label>Prompt</label> | ||
| <textarea id="play-prompt" placeholder="Type your prompt here..."></textarea> | ||
| </div> | ||
| <button onclick="testModel()" id="play-btn">Send</button> | ||
| <div id="play-results"></div> | ||
| <button class="btn" onclick="runTestAll()" id="test-all-btn">โก ไธ้ฎๆต่ฏๅ จ้จ / Test All</button> | ||
| <div id="test-results" style="margin-top:20px"></div> | ||
| <div id="test-recommend" style="margin-top:24px"></div> | ||
| </div> | ||
@@ -220,54 +306,115 @@ </div> | ||
| const API = ''; | ||
| let allModels = []; | ||
| let sortCol = null, sortAsc = true; | ||
| let providersData = []; | ||
| let statsData = {}; | ||
| // Navigation | ||
| document.querySelectorAll('.nav-item').forEach(el => { | ||
| el.addEventListener('click', () => { | ||
| document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); | ||
| document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); | ||
| el.classList.add('active'); | ||
| document.getElementById(el.dataset.page).classList.add('active'); | ||
| }); | ||
| // โโ Navigation (hash-based SPA) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | ||
| function navigate(page, sub) { | ||
| if (sub) { | ||
| window.location.hash = '#/' + page + '/' + sub; | ||
| } else { | ||
| window.location.hash = page === 'dashboard' ? '#/' : '#/' + page; | ||
| } | ||
| } | ||
| function handleRoute() { | ||
| const hash = window.location.hash || '#/'; | ||
| const parts = hash.replace('#/', '').split('/'); | ||
| let page = parts[0] || 'dashboard'; | ||
| const sub = parts[1]; | ||
| // Handle provider detail | ||
| if (page === 'models' && sub) { | ||
| page = 'provider-detail'; | ||
| showProviderDetail(sub); | ||
| } | ||
| document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); | ||
| document.querySelectorAll('.nav-item, .mn-item').forEach(n => n.classList.remove('active')); | ||
| const pageEl = document.getElementById(page); | ||
| if (pageEl) pageEl.classList.add('active'); | ||
| const navPage = page === 'provider-detail' ? 'models' : page; | ||
| document.querySelectorAll(`.nav-item[data-page="${navPage}"], .mn-item[data-page="${navPage}"]`).forEach(n => n.classList.add('active')); | ||
| } | ||
| document.querySelectorAll('.nav-item, .mn-item').forEach(el => { | ||
| el.addEventListener('click', () => navigate(el.dataset.page)); | ||
| }); | ||
| window.addEventListener('hashchange', handleRoute); | ||
| // Fetch helpers | ||
| // โโ API helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | ||
| async function api(path) { const r = await fetch(API + path); return r.json(); } | ||
| async function apiPost(path, body) { | ||
| const r = await fetch(API + path, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body) }); | ||
| return r.json(); | ||
| return (await fetch(API + path, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body) })).json(); | ||
| } | ||
| function fmt(n) { return n === 0 ? 'Free' : '$' + n.toFixed(2); } | ||
| function fmtCtx(n) { return n >= 1000000 ? (n/1000000).toFixed(1)+'M' : n >= 1000 ? (n/1000)+'K' : n; } | ||
| // Init | ||
| async function init() { | ||
| const data = await api('/api/models'); | ||
| allModels = data.models; | ||
| renderDashboard(); | ||
| renderModels(allModels); | ||
| populateModelSelects(); | ||
| async function apiPut(path, body) { | ||
| return (await fetch(API + path, { method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body) })).json(); | ||
| } | ||
| async function apiDelete(path) { | ||
| return (await fetch(API + path, { method:'DELETE' })).json(); | ||
| } | ||
| function fmt(n, cur) { return n === 0 ? 'ๅ ่ดน Free' : (cur === 'CNY' ? 'ยฅ' : '$') + n.toFixed(2); } | ||
| function fmtCtx(n) { return n >= 1000000 ? (n/1000000).toFixed(1)+'M' : n >= 1000 ? Math.round(n/1000)+'K' : n; } | ||
| function fmtTokens(n) { return n >= 1000000 ? (n/1000000).toFixed(1)+'M' : n >= 1000 ? Math.round(n/1000)+'K' : n; } | ||
| // Dashboard | ||
| // โโ Dashboard โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | ||
| async function renderDashboard() { | ||
| const provData = await api('/api/providers'); | ||
| const cards = document.getElementById('dash-cards'); | ||
| const totalModels = allModels.length; | ||
| const providers = provData.providers; | ||
| const freeModels = allModels.filter(m => m.inputPricePer1M === 0).length; | ||
| const cheapest = allModels.filter(m => m.inputPricePer1M > 0).sort((a,b) => a.inputPricePer1M - b.inputPricePer1M)[0]; | ||
| statsData = await api('/api/stats'); | ||
| const s = statsData; | ||
| cards.innerHTML = ` | ||
| <div class="card"><div class="label">Total Models</div><div class="value">${totalModels}</div><div class="sub">Across ${providers.length} providers</div></div> | ||
| <div class="card"><div class="label">Free Models</div><div class="value" style="color:var(--green)">${freeModels}</div><div class="sub">Local / Ollama</div></div> | ||
| <div class="card"><div class="label">Providers</div><div class="value">${providers.length}</div><div class="sub">Cloud + Local</div></div> | ||
| <div class="card"><div class="label">Cheapest Paid</div><div class="value" style="font-size:20px">${cheapest?.name||'โ'}</div><div class="sub">${cheapest ? fmt(cheapest.inputPricePer1M)+'/1M input' : ''}</div></div> | ||
| document.getElementById('dash-cards').innerHTML = ` | ||
| <div class="card"> | ||
| <div class="label">ๆฌๆๆถ่ This Month</div> | ||
| <div class="value" style="color:var(--accent)">${fmtTokens(s.month.tokens)}</div> | ||
| <div class="sub">tokens ยท $${s.month.cost.toFixed(2)}</div> | ||
| </div> | ||
| <div class="card"> | ||
| <div class="label">ๆฌๅจ This Week</div> | ||
| <div class="value">${fmtTokens(s.week.tokens)}</div> | ||
| <div class="sub">tokens ยท $${s.week.cost.toFixed(2)}</div> | ||
| </div> | ||
| <div class="card"> | ||
| <div class="label">ไปๆฅ Today</div> | ||
| <div class="value" style="color:var(--green)">${fmtTokens(s.today.tokens)}</div> | ||
| <div class="sub">tokens ยท $${s.today.cost.toFixed(2)}</div> | ||
| </div> | ||
| <div class="card"> | ||
| <div class="label">้ข็ฎ Budget</div> | ||
| <div class="value">$${s.budget}</div> | ||
| <div class="sub">ๅทฒ็จ ${s.budgetUsedPercent}%</div> | ||
| <div class="budget-bar"><div class="budget-fill" style="width:${Math.min(s.budgetUsedPercent,100)}%;background:${s.budgetUsedPercent>80?'var(--red)':s.budgetUsedPercent>50?'var(--yellow)':'var(--green)'}"></div></div> | ||
| </div> | ||
| `; | ||
| // Active models | ||
| document.getElementById('dash-active').innerHTML = ` | ||
| <div class="cards" style="grid-template-columns:repeat(auto-fit,minmax(280px,1fr))"> | ||
| <div class="card" style="border-left:3px solid var(--accent)"> | ||
| <div class="label">๐ค ๅฝๅ่ๅคฉๆจกๅ Chat Model</div> | ||
| <div class="value" style="font-size:20px">${s.activeChat}</div> | ||
| <div class="sub"><span class="badge badge-green">่ฟ่กไธญ Active</span></div> | ||
| </div> | ||
| <div class="card" style="border-left:3px solid var(--blue)"> | ||
| <div class="label">๐ ๅฝๅ Embedding ๆจกๅ</div> | ||
| <div class="value" style="font-size:20px">${s.activeEmbedding}</div> | ||
| <div class="sub"><span class="badge badge-green">่ฟ่กไธญ Active</span></div> | ||
| </div> | ||
| </div> | ||
| `; | ||
| // Provider overview | ||
| const provRes = await api('/api/providers'); | ||
| providersData = provRes.providers; | ||
| const grid = document.getElementById('dash-providers'); | ||
| grid.innerHTML = providers.map(p => ` | ||
| <div class="health-card"> | ||
| <h3>${p.name}</h3> | ||
| <div class="status">${p.models} model${p.models>1?'s':''}</div> | ||
| grid.innerHTML = providersData.slice(0, 8).map(p => ` | ||
| <div class="health-card" style="cursor:pointer" onclick="navigate('models','${p.name}')"> | ||
| <div class="hc-emoji">${p.emoji}</div> | ||
| <div class="hc-info"> | ||
| <div class="hc-name">${p.label}</div> | ||
| <div class="hc-status">${p.llmCount} LLM ยท ${p.embeddingCount} Embedding</div> | ||
| </div> | ||
| </div> | ||
@@ -277,100 +424,358 @@ `).join(''); | ||
| // Models table | ||
| function renderModels(models) { | ||
| const tbody = document.querySelector('#models-table tbody'); | ||
| tbody.innerHTML = models.map(m => `<tr> | ||
| <td><strong>${m.name}</strong></td> | ||
| <td><span class="provider-tag">${m.provider}</span></td> | ||
| <td>${fmt(m.inputPricePer1M)}</td> | ||
| <td>${fmt(m.outputPricePer1M)}</td> | ||
| <td>${fmtCtx(m.contextWindow)}</td> | ||
| <td style="color:var(--text-secondary);font-size:12px">${m.bestFor}</td> | ||
| </tr>`).join(''); | ||
| // โโ Models Market โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | ||
| async function renderModels() { | ||
| if (!providersData.length) { | ||
| const res = await api('/api/providers'); | ||
| providersData = res.providers; | ||
| } | ||
| const categories = { | ||
| 'free_local': { label: '๐ ๅ ่ดนๆฌๅฐ / Free Local', providers: [] }, | ||
| 'domestic': { label: '๐จ๐ณ ๅฝๅ ๆไพๅ / Domestic', providers: [] }, | ||
| 'overseas': { label: '๐ ๆตทๅคๆไพๅ / Overseas', providers: [] }, | ||
| }; | ||
| providersData.forEach(p => { | ||
| if (categories[p.category]) categories[p.category].providers.push(p); | ||
| }); | ||
| let html = ''; | ||
| for (const [, cat] of Object.entries(categories)) { | ||
| if (!cat.providers.length) continue; | ||
| html += `<div class="category-label">${cat.label}</div><div class="provider-grid">`; | ||
| html += cat.providers.map(p => ` | ||
| <div class="provider-card" onclick="navigate('models','${p.name}')"> | ||
| <div class="pc-head"> | ||
| <div class="pc-emoji">${p.emoji}</div> | ||
| <div> | ||
| <div class="pc-name">${p.label}</div> | ||
| <div class="pc-label">${p.name}</div> | ||
| </div> | ||
| </div> | ||
| <div class="pc-stats"> | ||
| <div class="pc-stat"><strong>${p.llmCount}</strong> LLM</div> | ||
| <div class="pc-stat"><strong>${p.embeddingCount}</strong> Embed</div> | ||
| <div class="pc-stat">${p.priceRange.max === 0 ? '<strong style="color:var(--green)">ๅ ่ดน</strong>' : fmt(p.priceRange.min, p.priceRange.currency) + ' ~ ' + fmt(p.priceRange.max, p.priceRange.currency)}</div> | ||
| </div> | ||
| <div class="pc-desc">${p.description}</div> | ||
| </div> | ||
| `).join(''); | ||
| html += '</div>'; | ||
| } | ||
| document.getElementById('models-content').innerHTML = html; | ||
| } | ||
| // Sort | ||
| document.querySelectorAll('#models-table th[data-sort]').forEach(th => { | ||
| th.addEventListener('click', () => { | ||
| const col = th.dataset.sort; | ||
| if (sortCol === col) sortAsc = !sortAsc; else { sortCol = col; sortAsc = true; } | ||
| const sorted = [...allModels].sort((a,b) => { | ||
| const va = a[col], vb = b[col]; | ||
| if (typeof va === 'number') return sortAsc ? va-vb : vb-va; | ||
| return sortAsc ? String(va).localeCompare(String(vb)) : String(vb).localeCompare(String(va)); | ||
| }); | ||
| renderModels(sorted); | ||
| // โโ Provider Detail โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | ||
| async function showProviderDetail(name) { | ||
| const container = document.getElementById('provider-detail-content'); | ||
| container.innerHTML = '<span class="spinner"></span> ๅ ่ฝฝไธญ...'; | ||
| const data = await api('/api/providers/' + name); | ||
| if (data.error) { | ||
| container.innerHTML = `<p>${data.error}</p>`; | ||
| return; | ||
| } | ||
| const p = data.provider; | ||
| const llmModels = data.models.filter(m => m.type === 'llm'); | ||
| const embModels = data.models.filter(m => m.type === 'embedding'); | ||
| let html = ` | ||
| <div class="back-link" onclick="navigate('models')">โ ่ฟๅๆจกๅๅธๅบ / Back to Market</div> | ||
| <div class="page-header"> | ||
| <h2>${p.emoji} ${p.label}</h2> | ||
| <p>${p.description} ยท <a href="${p.website}" target="_blank">${p.website}</a></p> | ||
| </div> | ||
| `; | ||
| if (llmModels.length) { | ||
| html += `<h3 style="margin-bottom:12px;font-size:15px">๐ค LLM ๆจกๅ (${llmModels.length})</h3><div class="model-list">`; | ||
| html += llmModels.map(m => ` | ||
| <div class="model-item"> | ||
| <div class="mi-info"> | ||
| <div class="mi-name">${m.name}</div> | ||
| <div class="mi-meta"> | ||
| <span>๐ ${fmtCtx(m.contextWindow)} ctx</span> | ||
| <span>๐ต ่พๅ ฅ ${fmt(m.inputPricePer1M, m.currency)}/1M</span> | ||
| <span>๐ต ่พๅบ ${fmt(m.outputPricePer1M, m.currency)}/1M</span> | ||
| <span>๐ฏ ${m.bestFor}</span> | ||
| </div> | ||
| <div class="mi-caps"> | ||
| ${m.capabilities.map(c => `<span class="cap-badge">${c === 'vision' ? '๐๏ธ Vision' : c === 'function_call' ? '๐ง Function Call' : '๐ Streaming'}</span>`).join('')} | ||
| </div> | ||
| </div> | ||
| <button class="btn btn-sm btn-outline" onclick="alert('ๆจกๅๅทฒ้ๆฉ: ${m.name}\\nModel selected: ${m.name}')">้ๆฉ Select</button> | ||
| </div> | ||
| `).join(''); | ||
| html += '</div>'; | ||
| } | ||
| if (embModels.length) { | ||
| html += `<h3 style="margin:24px 0 12px;font-size:15px">๐ Embedding ๆจกๅ (${embModels.length})</h3><div class="model-list">`; | ||
| html += embModels.map(m => ` | ||
| <div class="model-item"> | ||
| <div class="mi-info"> | ||
| <div class="mi-name">${m.name}</div> | ||
| <div class="mi-meta"> | ||
| <span>๐ ${fmtCtx(m.contextWindow)} ctx</span> | ||
| <span>๐ต ${fmt(m.inputPricePer1M, m.currency)}/1M tokens</span> | ||
| </div> | ||
| </div> | ||
| <button class="btn btn-sm btn-outline" onclick="alert('Embedding ๆจกๅๅทฒ้ๆฉ: ${m.name}')">้ๆฉ Select</button> | ||
| </div> | ||
| `).join(''); | ||
| html += '</div>'; | ||
| } | ||
| container.innerHTML = html; | ||
| } | ||
| // โโ Keys Management โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | ||
| async function renderKeys() { | ||
| const data = await api('/api/keys'); | ||
| const list = document.getElementById('keys-list'); | ||
| // Populate provider select | ||
| const provSelect = document.getElementById('key-provider'); | ||
| if (provSelect.options.length <= 1) { | ||
| const provRes = await api('/api/providers'); | ||
| provSelect.innerHTML = provRes.providers.map(p => `<option value="${p.name}">${p.emoji} ${p.label}</option>`).join(''); | ||
| } | ||
| if (!data.keys.length) { | ||
| list.innerHTML = `<div style="color:var(--text-muted);padding:20px;text-align:center">ๆๆ ้ ็ฝฎ็ API Key / No API keys configured</div>`; | ||
| return; | ||
| } | ||
| list.innerHTML = data.keys.map(k => ` | ||
| <div class="key-item"> | ||
| <div class="ki-info"> | ||
| <div class="ki-provider">${k.provider}</div> | ||
| <div class="ki-masked">${k.masked}</div> | ||
| <span class="badge ${k.status === 'configured' ? 'badge-green' : 'badge-blue'}">${k.status === 'configured' ? '็ฏๅขๅ้ ENV' : 'ไผ่ฏ Session'}</span> | ||
| </div> | ||
| <div class="ki-actions"> | ||
| <button class="btn btn-sm btn-outline" onclick="testKey('${k.provider}')">ๆต่ฏ Test</button> | ||
| ${k.status === 'session' ? `<button class="btn btn-sm btn-danger" onclick="deleteKey('${k.provider}')">ๅ ้ค Delete</button>` : ''} | ||
| </div> | ||
| </div> | ||
| `).join(''); | ||
| } | ||
| async function addKey() { | ||
| const provider = document.getElementById('key-provider').value; | ||
| const key = document.getElementById('key-value').value; | ||
| if (!key) return alert('่ฏท่พๅ ฅ API Key / Please enter API Key'); | ||
| await apiPost('/api/keys', { provider, key }); | ||
| document.getElementById('key-value').value = ''; | ||
| renderKeys(); | ||
| } | ||
| async function deleteKey(provider) { | ||
| if (!confirm(`็กฎ่ฎคๅ ้ค ${provider} ็ Key๏ผ/ Delete ${provider} key?`)) return; | ||
| await apiDelete('/api/keys/' + provider); | ||
| renderKeys(); | ||
| } | ||
| async function testKey(provider) { | ||
| const result = await apiPost('/api/keys/test', { provider }); | ||
| alert(`${provider}: ${result.available ? 'โ ๅฏ็จ Available' : 'โ ไธๅฏ็จ Unavailable'}\nๅปถ่ฟ Latency: ${result.latencyMs}ms${result.error ? '\n้่ฏฏ: ' + result.error : ''}`); | ||
| } | ||
| async function testAllKeys() { | ||
| const data = await apiPost('/api/keys/test-all', {}); | ||
| let msg = '่ฟๆฅๆต่ฏ็ปๆ / Test Results:\n\n'; | ||
| data.results.forEach(r => { | ||
| msg += `${r.available ? 'โ ' : 'โ'} ${r.provider}: ${r.latencyMs}ms${r.error ? ' - ' + r.error : ''}\n`; | ||
| }); | ||
| }); | ||
| alert(msg); | ||
| } | ||
| // Search | ||
| document.getElementById('model-search').addEventListener('input', e => { | ||
| const q = e.target.value.toLowerCase(); | ||
| renderModels(allModels.filter(m => m.name.includes(q) || m.provider.includes(q) || m.bestFor.toLowerCase().includes(q))); | ||
| }); | ||
| // โโ Cost Dashboard โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | ||
| // Recommend | ||
| async function doRecommend() { | ||
| const task = document.getElementById('rec-task').value; | ||
| const budget = document.getElementById('rec-budget').value; | ||
| const data = await api(`/api/recommend?task=${task}&budget=${budget}`); | ||
| const recs = data.recommendations; | ||
| document.getElementById('rec-results').innerHTML = recs.length === 0 | ||
| ? '<div class="result-box">No models match your criteria.</div>' | ||
| : '<table style="margin-top:16px"><thead><tr><th>Model</th><th>Provider</th><th>Quality</th><th>Speed</th><th>Cost/1K</th><th>Reason</th></tr></thead><tbody>' | ||
| + recs.map(r => `<tr><td><strong>${r.model}</strong></td><td><span class="provider-tag">${r.provider}</span></td><td>${r.quality}</td><td>${r.speed}</td><td>$${r.estimatedCostPer1kTokens}</td><td style="font-size:12px;color:var(--text-secondary)">${r.reason}</td></tr>`).join('') | ||
| + '</tbody></table>'; | ||
| async function renderCost() { | ||
| const costData = await api('/api/cost'); | ||
| const trendData = await api('/api/cost/trend'); | ||
| document.getElementById('cost-cards').innerHTML = ` | ||
| <div class="card"> | ||
| <div class="label">ไปๆฅ Today</div> | ||
| <div class="value" style="color:var(--green)">${fmtTokens(costData.today.tokens)}</div> | ||
| <div class="sub">$${costData.today.cost.toFixed(2)}</div> | ||
| </div> | ||
| <div class="card"> | ||
| <div class="label">ๆฌๅจ This Week</div> | ||
| <div class="value">${fmtTokens(costData.week.tokens)}</div> | ||
| <div class="sub">$${costData.week.cost.toFixed(2)}</div> | ||
| </div> | ||
| <div class="card"> | ||
| <div class="label">ๆฌๆ This Month</div> | ||
| <div class="value" style="color:var(--accent)">${fmtTokens(costData.month.tokens)}</div> | ||
| <div class="sub">$${costData.month.cost.toFixed(2)}</div> | ||
| </div> | ||
| <div class="card"> | ||
| <div class="label">้ข็ฎไฝ้ข Budget Left</div> | ||
| <div class="value">$${(costData.budget - costData.month.cost).toFixed(2)}</div> | ||
| <div class="sub">/ $${costData.budget}</div> | ||
| </div> | ||
| `; | ||
| // By model table | ||
| const tbody = document.querySelector('#cost-by-model tbody'); | ||
| tbody.innerHTML = costData.byModel.map(m => ` | ||
| <tr> | ||
| <td><strong>${m.model}</strong></td> | ||
| <td><span class="badge badge-purple">${m.provider}</span></td> | ||
| <td>${fmtTokens(m.tokens)}</td> | ||
| <td>$${m.cost.toFixed(4)}</td> | ||
| </tr> | ||
| `).join(''); | ||
| // Budget | ||
| const pct = Math.round((costData.month.cost / costData.budget) * 100); | ||
| document.getElementById('budget-display').innerHTML = ` | ||
| <div style="display:flex;justify-content:space-between;font-size:13px;color:var(--text-secondary)"> | ||
| <span>ๅทฒ็จ Used: $${costData.month.cost.toFixed(2)}</span> | ||
| <span>้ข็ฎ Budget: $${costData.budget}</span> | ||
| </div> | ||
| <div class="budget-bar"><div class="budget-fill" style="width:${Math.min(pct,100)}%;background:${pct>80?'var(--red)':pct>50?'var(--yellow)':'var(--green)'}"></div></div> | ||
| ${pct > 80 ? '<div style="color:var(--red);font-size:12px;margin-top:6px">โ ๏ธ ้ข็ฎไฝฟ็จ่ถ ่ฟ80%๏ผ/ Budget usage over 80%!</div>' : ''} | ||
| `; | ||
| document.getElementById('budget-input').value = costData.budget; | ||
| // SVG Trend chart | ||
| renderTrendChart(trendData.trend); | ||
| } | ||
| // Cost | ||
| function populateModelSelects() { | ||
| const opts = allModels.map(m => `<option value="${m.name}">${m.name} (${m.provider})</option>`).join(''); | ||
| document.getElementById('cost-model').innerHTML = opts; | ||
| document.getElementById('play-model').innerHTML = opts; | ||
| function renderTrendChart(trend) { | ||
| if (!trend || !trend.length) return; | ||
| const W = 700, H = 200, PAD = 40; | ||
| const maxCost = Math.max(...trend.map(t => t.cost), 0.01); | ||
| const points = trend.map((t, i) => { | ||
| const x = PAD + (i / (trend.length - 1)) * (W - PAD * 2); | ||
| const y = H - PAD - (t.cost / maxCost) * (H - PAD * 2); | ||
| return { x, y, ...t }; | ||
| }); | ||
| const line = points.map((p, i) => (i === 0 ? 'M' : 'L') + p.x.toFixed(1) + ',' + p.y.toFixed(1)).join(' '); | ||
| const area = line + ` L${points[points.length-1].x},${H-PAD} L${PAD},${H-PAD} Z`; | ||
| const labels = [0, 7, 14, 21, 29].filter(i => i < trend.length).map(i => { | ||
| const p = points[i]; | ||
| return `<text x="${p.x}" y="${H-10}" fill="#55556a" font-size="10" text-anchor="middle">${trend[i].date.slice(5)}</text>`; | ||
| }).join(''); | ||
| const yLabels = [0, 0.25, 0.5, 0.75, 1].map(f => { | ||
| const val = (maxCost * f).toFixed(3); | ||
| const y = H - PAD - f * (H - PAD * 2); | ||
| return `<text x="${PAD-5}" y="${y+3}" fill="#55556a" font-size="10" text-anchor="end">$${val}</text><line x1="${PAD}" y1="${y}" x2="${W-PAD}" y2="${y}" stroke="#2a2a3e" stroke-dasharray="4"/>`; | ||
| }).join(''); | ||
| document.getElementById('trend-chart').innerHTML = ` | ||
| <svg viewBox="0 0 ${W} ${H}" style="width:100%;max-width:${W}px"> | ||
| ${yLabels} | ||
| <path d="${area}" fill="url(#grad)" opacity="0.3"/> | ||
| <path d="${line}" fill="none" stroke="#6366f1" stroke-width="2"/> | ||
| ${points.map(p => `<circle cx="${p.x}" cy="${p.y}" r="3" fill="#6366f1"><title>${p.date}: $${p.cost}</title></circle>`).join('')} | ||
| ${labels} | ||
| <defs><linearGradient id="grad" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#6366f1"/><stop offset="100%" stop-color="#6366f1" stop-opacity="0"/></linearGradient></defs> | ||
| </svg> | ||
| `; | ||
| } | ||
| async function calcCost() { | ||
| const model = document.getElementById('cost-model').value; | ||
| const tokensPerDay = parseInt(document.getElementById('cost-tokens').value); | ||
| const days = parseInt(document.getElementById('cost-days').value); | ||
| const totalTokens = tokensPerDay * days; | ||
| const data = await api(`/api/cost?model=${encodeURIComponent(model)}&tokens=${totalTokens}`); | ||
| const est = data.estimate; | ||
| document.getElementById('cost-results').innerHTML = ` | ||
| <div class="cards" style="margin-top:16px"> | ||
| <div class="card"><div class="label">Total Cost</div><div class="value" style="color:var(--accent)">$${est.cost.toFixed(4)}</div><div class="sub">${tokensPerDay.toLocaleString()} tokens/day ร ${days} days</div></div> | ||
| <div class="card"><div class="label">Input Cost</div><div class="value">$${est.breakdown.input.toFixed(4)}</div></div> | ||
| <div class="card"><div class="label">Output Cost</div><div class="value">$${est.breakdown.output.toFixed(4)}</div></div> | ||
| <div class="card"><div class="label">Daily Cost</div><div class="value">$${(est.cost/days).toFixed(6)}</div></div> | ||
| </div>`; | ||
| async function setBudget() { | ||
| const val = parseFloat(document.getElementById('budget-input').value); | ||
| if (!val || val <= 0) return alert('่ฏท่พๅ ฅๆๆ้ข็ฎ / Enter a valid budget'); | ||
| await apiPut('/api/cost/budget', { budget: val }); | ||
| renderCost(); | ||
| } | ||
| // Health | ||
| async function checkAllProviders() { | ||
| const grid = document.getElementById('health-grid'); | ||
| const providers = ['openai','anthropic','gemini','deepseek','moonshot','zhipu','ollama']; | ||
| grid.innerHTML = providers.map(p => `<div class="health-card" id="hc-${p}"><h3>${p}</h3><div class="status"><span class="spinner"></span> Checking...</div></div>`).join(''); | ||
| for (const p of providers) { | ||
| const data = await api(`/api/providers/check?name=${p}`); | ||
| const el = document.getElementById('hc-' + p); | ||
| const badge = data.available ? '<span class="badge badge-green">Available</span>' : '<span class="badge badge-red">Unavailable</span>'; | ||
| el.innerHTML = `<h3>${p}</h3><div class="status">${badge} ${data.latencyMs}ms${data.error ? ' โ '+data.error : ''}</div>`; | ||
| // โโ Connection Test โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | ||
| async function runTestAll() { | ||
| const btn = document.getElementById('test-all-btn'); | ||
| btn.disabled = true; | ||
| btn.textContent = 'โณ ๆต่ฏไธญ... Testing...'; | ||
| const results = document.getElementById('test-results'); | ||
| results.innerHTML = '<div class="health-grid" id="test-grid"></div>'; | ||
| // Show placeholders | ||
| if (!providersData.length) { | ||
| const res = await api('/api/providers'); | ||
| providersData = res.providers; | ||
| } | ||
| const grid = document.getElementById('test-grid'); | ||
| grid.innerHTML = providersData.map(p => ` | ||
| <div class="health-card" id="test-${p.name}"> | ||
| <div class="hc-emoji">${p.emoji}</div> | ||
| <div class="hc-info"> | ||
| <div class="hc-name">${p.label}</div> | ||
| <div class="hc-status"><span class="spinner"></span> ๆต่ฏไธญ...</div> | ||
| </div> | ||
| </div> | ||
| `).join(''); | ||
| const data = await apiPost('/api/keys/test-all', {}); | ||
| const testResults = []; | ||
| data.results.forEach(r => { | ||
| const meta = providersData.find(p => p.name === r.provider); | ||
| const el = document.getElementById('test-' + r.provider); | ||
| if (el) { | ||
| el.querySelector('.hc-status').innerHTML = r.available | ||
| ? `<span class="badge badge-green">โ ๅฏ็จ</span> <span style="color:var(--text-muted);font-size:12px">${r.latencyMs}ms</span>` | ||
| : `<span class="badge badge-red">โ ไธๅฏ็จ</span> <span style="color:var(--text-muted);font-size:12px">${r.error || ''}</span>`; | ||
| } | ||
| if (r.available) testResults.push({ ...r, meta }); | ||
| }); | ||
| // Remove placeholders for untested providers | ||
| providersData.forEach(p => { | ||
| if (!data.results.find(r => r.provider === p.name)) { | ||
| const el = document.getElementById('test-' + p.name); | ||
| if (el) el.querySelector('.hc-status').innerHTML = '<span class="badge badge-yellow">โญ๏ธ ๆชๆต่ฏ</span>'; | ||
| } | ||
| }); | ||
| // Recommendations | ||
| if (testResults.length) { | ||
| const fastest = testResults.sort((a, b) => a.latencyMs - b.latencyMs)[0]; | ||
| document.getElementById('test-recommend').innerHTML = ` | ||
| <div style="background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:20px"> | ||
| <h3 style="font-size:15px;margin-bottom:12px">๐ ๆจ่ / Recommendations</h3> | ||
| <div class="cards" style="grid-template-columns:repeat(auto-fit,minmax(200px,1fr))"> | ||
| <div class="card" style="border-left:3px solid var(--green)"> | ||
| <div class="label">โก ๆๅฟซ Fastest</div> | ||
| <div class="value" style="font-size:18px">${fastest.provider}</div> | ||
| <div class="sub">${fastest.latencyMs}ms</div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| `; | ||
| } | ||
| btn.disabled = false; | ||
| btn.textContent = 'โก ไธ้ฎๆต่ฏๅ จ้จ / Test All'; | ||
| } | ||
| // Playground | ||
| async function testModel() { | ||
| const model = document.getElementById('play-model').value; | ||
| const prompt = document.getElementById('play-prompt').value; | ||
| if (!prompt) return; | ||
| document.getElementById('play-btn').disabled = true; | ||
| document.getElementById('play-results').innerHTML = '<div class="result-box"><span class="spinner"></span> Sending...</div>'; | ||
| const start = Date.now(); | ||
| const data = await apiPost('/api/test', { model, prompt }); | ||
| const latency = Date.now() - start; | ||
| document.getElementById('play-results').innerHTML = ` | ||
| <div class="result-box">${data.response || data.error || 'No response'}</div> | ||
| <div style="margin-top:8px;font-size:12px;color:var(--text-muted)">Model: ${data.model} | Latency: ${latency}ms | Input tokens: ${data.tokens?.input||0}</div>`; | ||
| document.getElementById('play-btn').disabled = false; | ||
| // โโ Init โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | ||
| async function init() { | ||
| await renderDashboard(); | ||
| await renderModels(); | ||
| handleRoute(); | ||
| } | ||
| // Lazy load pages on navigate | ||
| window.addEventListener('hashchange', () => { | ||
| const hash = window.location.hash || '#/'; | ||
| const page = hash.replace('#/', '').split('/')[0] || 'dashboard'; | ||
| if (page === 'keys') renderKeys(); | ||
| if (page === 'cost') renderCost(); | ||
| }); | ||
| init(); | ||
@@ -377,0 +782,0 @@ </script> |
+312
-43
@@ -19,2 +19,3 @@ /** | ||
| import { checkProvider } from '../health.js'; | ||
| import { getPricing } from '../cost/index.js'; | ||
@@ -26,32 +27,138 @@ export interface KitsUIConfig { | ||
| // Model catalog for /api/models | ||
| const MODEL_CATALOG = [ | ||
| { name: 'gpt-4o', provider: 'openai', inputPricePer1M: 2.50, outputPricePer1M: 10.00, contextWindow: 128000, bestFor: 'General, vision, coding' }, | ||
| { name: 'gpt-4o-mini', provider: 'openai', inputPricePer1M: 0.15, outputPricePer1M: 0.60, contextWindow: 128000, bestFor: 'Simple tasks, fast' }, | ||
| { name: 'text-embedding-3-small', provider: 'openai', inputPricePer1M: 0.02, outputPricePer1M: 0, contextWindow: 8191, bestFor: 'Embeddings' }, | ||
| { name: 'text-embedding-3-large', provider: 'openai', inputPricePer1M: 0.13, outputPricePer1M: 0, contextWindow: 8191, bestFor: 'High-dim embeddings' }, | ||
| { name: 'claude-3.5-sonnet', provider: 'anthropic', inputPricePer1M: 3.00, outputPricePer1M: 15.00, contextWindow: 200000, bestFor: 'Reasoning, coding' }, | ||
| { name: 'claude-3-haiku', provider: 'anthropic', inputPricePer1M: 0.25, outputPricePer1M: 1.25, contextWindow: 200000, bestFor: 'Fast, cheap' }, | ||
| { name: 'gemini-1.5-pro', provider: 'gemini', inputPricePer1M: 1.25, outputPricePer1M: 5.00, contextWindow: 2000000, bestFor: 'Huge context, analysis' }, | ||
| { name: 'gemini-2.5-flash', provider: 'gemini', inputPricePer1M: 0.15, outputPricePer1M: 0.60, contextWindow: 1000000, bestFor: 'Fast, cheap' }, | ||
| { name: 'deepseek-chat', provider: 'deepseek', inputPricePer1M: 0.14, outputPricePer1M: 0.28, contextWindow: 128000, bestFor: 'General, cheap GPT-4 class' }, | ||
| { name: 'deepseek-coder-v2', provider: 'deepseek', inputPricePer1M: 0.14, outputPricePer1M: 0.28, contextWindow: 128000, bestFor: 'Coding' }, | ||
| { name: 'deepseek-reasoner', provider: 'deepseek', inputPricePer1M: 0.55, outputPricePer1M: 2.19, contextWindow: 64000, bestFor: 'Deep reasoning' }, | ||
| { name: 'moonshot-v1-8k', provider: 'moonshot', inputPricePer1M: 0.17, outputPricePer1M: 0.17, contextWindow: 8000, bestFor: 'Chinese LLM' }, | ||
| { name: 'glm-4-flash', provider: 'zhipu', inputPricePer1M: 0.01, outputPricePer1M: 0.01, contextWindow: 128000, bestFor: 'Near-free Chinese' }, | ||
| { name: 'glm-4-plus', provider: 'zhipu', inputPricePer1M: 7.00, outputPricePer1M: 7.00, contextWindow: 128000, bestFor: 'Premium Chinese' }, | ||
| { name: 'qwen-turbo', provider: 'dashscope', inputPricePer1M: 0.04, outputPricePer1M: 0.08, contextWindow: 128000, bestFor: 'Fast, cheap Alibaba' }, | ||
| { name: 'qwen-plus', provider: 'dashscope', inputPricePer1M: 0.11, outputPricePer1M: 0.28, contextWindow: 128000, bestFor: 'Balanced Alibaba' }, | ||
| { name: 'qwen2.5', provider: 'ollama', inputPricePer1M: 0, outputPricePer1M: 0, contextWindow: 32000, bestFor: 'Free local multilingual' }, | ||
| { name: 'llama3', provider: 'ollama', inputPricePer1M: 0, outputPricePer1M: 0, contextWindow: 8000, bestFor: 'Free local general' }, | ||
| { name: 'deepseek-coder-v2:local', provider: 'ollama', inputPricePer1M: 0, outputPricePer1M: 0, contextWindow: 128000, bestFor: 'Free local coding' }, | ||
| { name: 'nomic-embed-text', provider: 'ollama', inputPricePer1M: 0, outputPricePer1M: 0, contextWindow: 8192, bestFor: 'Free local embeddings' }, | ||
| // โโ Provider Metadata โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | ||
| export interface ProviderMeta { | ||
| name: string; | ||
| label: string; | ||
| emoji: string; | ||
| category: 'free_local' | 'domestic' | 'overseas'; | ||
| description: string; | ||
| website: string; | ||
| } | ||
| const PROVIDER_META: ProviderMeta[] = [ | ||
| { name: 'ollama', label: 'Ollama', emoji: '๐ฆ', category: 'free_local', description: 'ๆฌๅฐๅ ่ดน่ฟ่กๅผๆบๆจกๅ / Run open-source models locally for free', website: 'https://ollama.com' }, | ||
| { name: 'deepseek', label: 'DeepSeek', emoji: '๐', category: 'domestic', description: '้ซๆงไปทๆฏๆจ็ๆจกๅ / Cost-effective reasoning models', website: 'https://deepseek.com' }, | ||
| { name: 'dashscope', label: '้ไนๅ้ฎ Qwen', emoji: 'โ๏ธ', category: 'domestic', description: '้ฟ้ไบๅคงๆจกๅ / Alibaba Cloud LLM', website: 'https://dashscope.aliyun.com' }, | ||
| { name: 'zhipu', label: 'ๆบ่ฐฑ GLM', emoji: '๐ง ', category: 'domestic', description: 'ๆธ ๅ็ณปๅคงๆจกๅ / Tsinghua-backed LLM', website: 'https://open.bigmodel.cn' }, | ||
| { name: 'moonshot', label: 'ๆไนๆ้ข Kimi', emoji: '๐', category: 'domestic', description: '้ฟไธไธๆๅคงๆจกๅ / Long-context LLM', website: 'https://moonshot.cn' }, | ||
| { name: 'minimax', label: 'MiniMax', emoji: '๐ต', category: 'domestic', description: 'ๅคๆจกๆๅคงๆจกๅ / Multimodal LLM', website: 'https://minimax.chat' }, | ||
| { name: 'yi', label: '้ถไธไธ็ฉ Yi', emoji: '1๏ธโฃ', category: 'domestic', description: 'ๆๅผๅคๅข้ๅคงๆจกๅ / Kai-Fu Lee LLM', website: 'https://01.ai' }, | ||
| { name: 'baichuan', label: '็พๅทๆบ่ฝ', emoji: '๐', category: 'domestic', description: '็พๅทๅคงๆจกๅ / Baichuan LLM', website: 'https://baichuan-ai.com' }, | ||
| { name: 'siliconflow', label: '็ก ๅบๆตๅจ', emoji: '๐', category: 'domestic', description: 'ๆจกๅๆจ็ๅนณๅฐ / Model inference platform', website: 'https://siliconflow.cn' }, | ||
| { name: 'stepfun', label: '้ถ่ทๆ่พฐ', emoji: 'โญ', category: 'domestic', description: 'ไธไบฟๅๆฐๅคงๆจกๅ / Trillion-parameter LLM', website: 'https://stepfun.com' }, | ||
| { name: 'openai', label: 'OpenAI', emoji: '๐ค', category: 'overseas', description: 'GPT ็ณปๅ / GPT series', website: 'https://openai.com' }, | ||
| { name: 'anthropic', label: 'Anthropic', emoji: '๐งฌ', category: 'overseas', description: 'Claude ็ณปๅ / Claude series', website: 'https://anthropic.com' }, | ||
| { name: 'gemini', label: 'Google Gemini', emoji: '๐ซ', category: 'overseas', description: 'Gemini ็ณปๅ / Gemini series', website: 'https://ai.google.dev' }, | ||
| { name: 'grok', label: 'xAI Grok', emoji: '๐', category: 'overseas', description: 'Grok ็ณปๅ / Grok series', website: 'https://x.ai' }, | ||
| { name: 'cohere', label: 'Cohere', emoji: '๐', category: 'overseas', description: 'Command ็ณปๅ / Command series', website: 'https://cohere.com' }, | ||
| { name: 'fireworks', label: 'Fireworks AI', emoji: '๐', category: 'overseas', description: 'ๅฟซ้ๆจ็ๅนณๅฐ / Fast inference platform', website: 'https://fireworks.ai' }, | ||
| { name: 'together', label: 'Together AI', emoji: '๐ค', category: 'overseas', description: 'ๅผๆบๆจกๅๆ็ฎก / Open-source model hosting', website: 'https://together.ai' }, | ||
| { name: 'groq', label: 'Groq', emoji: 'โก', category: 'overseas', description: '่ถ ไฝๅปถ่ฟๆจ็ / Ultra-low latency inference', website: 'https://groq.com' }, | ||
| { name: 'perplexity', label: 'Perplexity', emoji: '๐', category: 'overseas', description: 'ๆ็ดขๅขๅผบๆจกๅ / Search-augmented models', website: 'https://perplexity.ai' }, | ||
| { name: 'jina', label: 'Jina AI', emoji: '๐', category: 'overseas', description: 'Embedding ไธๅฎถ / Embedding specialist', website: 'https://jina.ai' }, | ||
| { name: 'voyage', label: 'Voyage AI', emoji: '๐ข', category: 'overseas', description: 'Embedding ไธๅฎถ / Embedding specialist', website: 'https://voyageai.com' }, | ||
| ]; | ||
| const PROVIDERS = ['openai', 'anthropic', 'gemini', 'deepseek', 'moonshot', 'zhipu', 'ollama', 'dashscope']; | ||
| // โโ Model Catalog โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | ||
| export interface ModelEntry { | ||
| name: string; | ||
| provider: string; | ||
| type: 'llm' | 'embedding'; | ||
| inputPricePer1M: number; | ||
| outputPricePer1M: number; | ||
| currency: string; | ||
| contextWindow: number; | ||
| bestFor: string; | ||
| capabilities: string[]; // vision, function_call, streaming | ||
| } | ||
| const MODEL_CATALOG: ModelEntry[] = [ | ||
| // OpenAI LLM | ||
| { name: 'gpt-4o', provider: 'openai', type: 'llm', inputPricePer1M: 2.50, outputPricePer1M: 10.00, currency: 'USD', contextWindow: 128000, bestFor: 'General, vision, coding', capabilities: ['vision','function_call','streaming'] }, | ||
| { name: 'gpt-4o-mini', provider: 'openai', type: 'llm', inputPricePer1M: 0.15, outputPricePer1M: 0.60, currency: 'USD', contextWindow: 128000, bestFor: 'Simple tasks, fast', capabilities: ['vision','function_call','streaming'] }, | ||
| // Anthropic LLM | ||
| { name: 'claude-3.5-sonnet', provider: 'anthropic', type: 'llm', inputPricePer1M: 3.00, outputPricePer1M: 15.00, currency: 'USD', contextWindow: 200000, bestFor: 'Reasoning, coding', capabilities: ['vision','function_call','streaming'] }, | ||
| { name: 'claude-3-haiku', provider: 'anthropic', type: 'llm', inputPricePer1M: 0.25, outputPricePer1M: 1.25, currency: 'USD', contextWindow: 200000, bestFor: 'Fast, cheap', capabilities: ['streaming'] }, | ||
| // Gemini LLM | ||
| { name: 'gemini-2.5-pro', provider: 'gemini', type: 'llm', inputPricePer1M: 1.25, outputPricePer1M: 10.00, currency: 'USD', contextWindow: 2000000, bestFor: 'Huge context, analysis', capabilities: ['vision','function_call','streaming'] }, | ||
| { name: 'gemini-2.5-flash', provider: 'gemini', type: 'llm', inputPricePer1M: 0.15, outputPricePer1M: 0.60, currency: 'USD', contextWindow: 1000000, bestFor: 'Fast, cheap', capabilities: ['vision','function_call','streaming'] }, | ||
| // DeepSeek LLM | ||
| { name: 'deepseek-chat', provider: 'deepseek', type: 'llm', inputPricePer1M: 0.14, outputPricePer1M: 0.28, currency: 'USD', contextWindow: 128000, bestFor: 'General, cheap GPT-4 class', capabilities: ['function_call','streaming'] }, | ||
| { name: 'deepseek-coder-v2', provider: 'deepseek', type: 'llm', inputPricePer1M: 0.14, outputPricePer1M: 0.28, currency: 'USD', contextWindow: 128000, bestFor: 'Coding', capabilities: ['function_call','streaming'] }, | ||
| { name: 'deepseek-reasoner', provider: 'deepseek', type: 'llm', inputPricePer1M: 0.55, outputPricePer1M: 2.19, currency: 'USD', contextWindow: 64000, bestFor: 'Deep reasoning', capabilities: ['streaming'] }, | ||
| // DashScope (Qwen) LLM | ||
| { name: 'qwen-max', provider: 'dashscope', type: 'llm', inputPricePer1M: 2.40, outputPricePer1M: 9.60, currency: 'CNY', contextWindow: 128000, bestFor: 'Premium Chinese LLM', capabilities: ['function_call','streaming'] }, | ||
| { name: 'qwen-plus', provider: 'dashscope', type: 'llm', inputPricePer1M: 0.80, outputPricePer1M: 2.00, currency: 'CNY', contextWindow: 128000, bestFor: 'Balanced Alibaba', capabilities: ['function_call','streaming'] }, | ||
| { name: 'qwen-turbo', provider: 'dashscope', type: 'llm', inputPricePer1M: 0.30, outputPricePer1M: 0.60, currency: 'CNY', contextWindow: 128000, bestFor: 'Fast, cheap Alibaba', capabilities: ['function_call','streaming'] }, | ||
| // Zhipu (GLM) LLM | ||
| { name: 'glm-4-plus', provider: 'zhipu', type: 'llm', inputPricePer1M: 50.00, outputPricePer1M: 50.00, currency: 'CNY', contextWindow: 128000, bestFor: 'Premium Chinese', capabilities: ['function_call','streaming'] }, | ||
| { name: 'glm-4-flash', provider: 'zhipu', type: 'llm', inputPricePer1M: 0.10, outputPricePer1M: 0.10, currency: 'CNY', contextWindow: 128000, bestFor: 'Near-free Chinese', capabilities: ['function_call','streaming'] }, | ||
| // Moonshot LLM | ||
| { name: 'moonshot-v1-auto', provider: 'moonshot', type: 'llm', inputPricePer1M: 12.00, outputPricePer1M: 12.00, currency: 'CNY', contextWindow: 128000, bestFor: 'Long context Chinese', capabilities: ['streaming'] }, | ||
| // MiniMax LLM | ||
| { name: 'MiniMax-Text-01', provider: 'minimax', type: 'llm', inputPricePer1M: 1.00, outputPricePer1M: 8.00, currency: 'CNY', contextWindow: 1000000, bestFor: 'Long context multimodal', capabilities: ['streaming'] }, | ||
| // Grok LLM | ||
| { name: 'grok-3', provider: 'grok', type: 'llm', inputPricePer1M: 3.00, outputPricePer1M: 15.00, currency: 'USD', contextWindow: 131072, bestFor: 'General reasoning', capabilities: ['function_call','streaming'] }, | ||
| // Cohere LLM | ||
| { name: 'command-r-plus', provider: 'cohere', type: 'llm', inputPricePer1M: 2.50, outputPricePer1M: 10.00, currency: 'USD', contextWindow: 128000, bestFor: 'RAG, enterprise', capabilities: ['function_call','streaming'] }, | ||
| // Yi LLM | ||
| { name: 'yi-large', provider: 'yi', type: 'llm', inputPricePer1M: 20.00, outputPricePer1M: 20.00, currency: 'CNY', contextWindow: 32768, bestFor: 'Chinese reasoning', capabilities: ['streaming'] }, | ||
| // Baichuan LLM | ||
| { name: 'Baichuan4', provider: 'baichuan', type: 'llm', inputPricePer1M: 100.00,outputPricePer1M: 100.00,currency: 'CNY', contextWindow: 128000, bestFor: 'Premium Chinese', capabilities: ['streaming'] }, | ||
| // SiliconFlow LLM | ||
| { name: 'deepseek-ai/DeepSeek-V3', provider: 'siliconflow', type: 'llm', inputPricePer1M: 2.00, outputPricePer1M: 8.00, currency: 'CNY', contextWindow: 128000, bestFor: 'DeepSeek via SiliconFlow', capabilities: ['streaming'] }, | ||
| // StepFun LLM | ||
| { name: 'step-2-16k', provider: 'stepfun', type: 'llm', inputPricePer1M: 38.00, outputPricePer1M: 120.00,currency: 'CNY', contextWindow: 16000, bestFor: 'Premium reasoning', capabilities: ['streaming'] }, | ||
| // Fireworks LLM | ||
| { name: 'llama-v3p1-70b-instruct', provider: 'fireworks', type: 'llm', inputPricePer1M: 0.90, outputPricePer1M: 0.90, currency: 'USD', contextWindow: 131072, bestFor: 'Open-source hosting', capabilities: ['function_call','streaming'] }, | ||
| // Together LLM | ||
| { name: 'Meta-Llama-3.1-70B-Instruct-Turbo', provider: 'together', type: 'llm', inputPricePer1M: 0.88, outputPricePer1M: 0.88, currency: 'USD', contextWindow: 131072, bestFor: 'Open-source hosting', capabilities: ['function_call','streaming'] }, | ||
| // Groq LLM | ||
| { name: 'llama-3.3-70b-versatile', provider: 'groq', type: 'llm', inputPricePer1M: 0.59, outputPricePer1M: 0.79, currency: 'USD', contextWindow: 131072, bestFor: 'Ultra-fast inference', capabilities: ['function_call','streaming'] }, | ||
| // Perplexity LLM | ||
| { name: 'sonar-pro', provider: 'perplexity', type: 'llm', inputPricePer1M: 3.00, outputPricePer1M: 15.00, currency: 'USD', contextWindow: 128000, bestFor: 'Search-augmented', capabilities: ['streaming'] }, | ||
| // Ollama LLM | ||
| { name: 'qwen2.5', provider: 'ollama', type: 'llm', inputPricePer1M: 0, outputPricePer1M: 0, currency: 'USD', contextWindow: 32000, bestFor: 'Free local multilingual', capabilities: ['streaming'] }, | ||
| { name: 'llama3', provider: 'ollama', type: 'llm', inputPricePer1M: 0, outputPricePer1M: 0, currency: 'USD', contextWindow: 8000, bestFor: 'Free local general', capabilities: ['streaming'] }, | ||
| { name: 'deepseek-coder-v2:local', provider: 'ollama', type: 'llm', inputPricePer1M: 0, outputPricePer1M: 0, currency: 'USD', contextWindow: 128000, bestFor: 'Free local coding', capabilities: ['streaming'] }, | ||
| // โโ Embedding Models โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | ||
| { name: 'text-embedding-3-large', provider: 'openai', type: 'embedding', inputPricePer1M: 0.13, outputPricePer1M: 0, currency: 'USD', contextWindow: 8191, bestFor: 'High-dim embeddings', capabilities: [] }, | ||
| { name: 'text-embedding-3-small', provider: 'openai', type: 'embedding', inputPricePer1M: 0.02, outputPricePer1M: 0, currency: 'USD', contextWindow: 8191, bestFor: 'Cheap embeddings', capabilities: [] }, | ||
| { name: 'gemini-embedding-001', provider: 'gemini', type: 'embedding', inputPricePer1M: 0, outputPricePer1M: 0, currency: 'USD', contextWindow: 3072, bestFor: 'Free embeddings', capabilities: [] }, | ||
| { name: 'deepseek-embedding-v2', provider: 'deepseek', type: 'embedding', inputPricePer1M: 0.07, outputPricePer1M: 0, currency: 'USD', contextWindow: 8192, bestFor: 'Cheap embeddings', capabilities: [] }, | ||
| { name: 'text-embedding-v3', provider: 'dashscope', type: 'embedding', inputPricePer1M: 0.70, outputPricePer1M: 0, currency: 'CNY', contextWindow: 8192, bestFor: 'Chinese embeddings', capabilities: [] }, | ||
| { name: 'embedding-3', provider: 'zhipu', type: 'embedding', inputPricePer1M: 0.50, outputPricePer1M: 0, currency: 'CNY', contextWindow: 8192, bestFor: 'Chinese embeddings', capabilities: [] }, | ||
| { name: 'embed-english-v3.0', provider: 'cohere', type: 'embedding', inputPricePer1M: 0.10, outputPricePer1M: 0, currency: 'USD', contextWindow: 512, bestFor: 'English embeddings', capabilities: [] }, | ||
| { name: 'jina-embeddings-v3', provider: 'jina', type: 'embedding', inputPricePer1M: 0.02, outputPricePer1M: 0, currency: 'USD', contextWindow: 8192, bestFor: 'Multilingual embeddings', capabilities: [] }, | ||
| { name: 'voyage-3', provider: 'voyage', type: 'embedding', inputPricePer1M: 0.06, outputPricePer1M: 0, currency: 'USD', contextWindow: 32000,bestFor: 'Code & text embeddings', capabilities: [] }, | ||
| { name: 'nomic-embed-text', provider: 'ollama', type: 'embedding', inputPricePer1M: 0, outputPricePer1M: 0, currency: 'USD', contextWindow: 8192, bestFor: 'Free local embeddings', capabilities: [] }, | ||
| ]; | ||
| // โโ In-memory state โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | ||
| const apiKeys = new Map<string, { key: string; addedAt: string }>(); | ||
| let budgetLimit = 100; // USD | ||
| const usageStats = { | ||
| today: { tokens: 45200, cost: 0.12 }, | ||
| week: { tokens: 312500, cost: 0.85 }, | ||
| month: { tokens: 1250000, cost: 3.42 }, | ||
| byModel: [ | ||
| { model: 'deepseek-chat', provider: 'deepseek', tokens: 850000, cost: 1.19 }, | ||
| { model: 'gpt-4o-mini', provider: 'openai', tokens: 250000, cost: 0.45 }, | ||
| { model: 'gemini-2.5-flash', provider: 'gemini', tokens: 100000, cost: 0.12 }, | ||
| { model: 'text-embedding-3-small', provider: 'openai', tokens: 50000, cost: 0.001 }, | ||
| ], | ||
| activeChat: 'deepseek-chat', | ||
| activeEmbedding: 'text-embedding-3-small', | ||
| }; | ||
| // โโ Helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | ||
| function corsHeaders(): Record<string, string> { | ||
| return { | ||
| 'Access-Control-Allow-Origin': '*', | ||
| 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', | ||
| 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', | ||
| 'Access-Control-Allow-Headers': 'Content-Type', | ||
@@ -76,2 +183,17 @@ 'Content-Type': 'application/json', | ||
| function maskKey(key: string): string { | ||
| if (key.length <= 8) return key.slice(0, 4) + '****'; | ||
| return key.slice(0, 4) + '****' + key.slice(-4); | ||
| } | ||
| function getProviderModels(providerName: string): ModelEntry[] { | ||
| return MODEL_CATALOG.filter(m => m.provider === providerName); | ||
| } | ||
| function getProviderMeta(providerName: string): ProviderMeta | undefined { | ||
| return PROVIDER_META.find(p => p.name === providerName); | ||
| } | ||
| // โโ Main Class โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | ||
| export class KitsUI { | ||
@@ -92,3 +214,2 @@ private config: KitsUIConfig; | ||
| this.server = http.createServer(async (req, res) => { | ||
| // Handle CORS preflight | ||
| if (req.method === 'OPTIONS') { | ||
@@ -104,6 +225,18 @@ res.writeHead(204, corsHeaders()); | ||
| try { | ||
| // API routes | ||
| // โโ API Routes โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | ||
| // GET /api/models | ||
| if (pathname === '/api/models' && req.method === 'GET') { | ||
| jsonResponse(res, { models: MODEL_CATALOG, total: MODEL_CATALOG.length }); | ||
| } else if (pathname === '/api/recommend' && req.method === 'GET') { | ||
| } | ||
| // GET /api/stats | ||
| else if (pathname === '/api/stats' && req.method === 'GET') { | ||
| jsonResponse(res, { | ||
| ...usageStats, | ||
| budget: budgetLimit, | ||
| budgetUsedPercent: Math.round((usageStats.month.cost / budgetLimit) * 100), | ||
| }); | ||
| } | ||
| // GET /api/recommend | ||
| else if (pathname === '/api/recommend' && req.method === 'GET') { | ||
| const task = (parsedUrl.searchParams.get('task') || 'chat') as any; | ||
@@ -113,18 +246,150 @@ const budget = (parsedUrl.searchParams.get('budget') || 'high') as any; | ||
| jsonResponse(res, { recommendations: results, task, budget }); | ||
| } else if (pathname === '/api/cost' && req.method === 'GET') { | ||
| const model = parsedUrl.searchParams.get('model') || 'gpt-4o'; | ||
| const tokens = parseInt(parsedUrl.searchParams.get('tokens') || '1000', 10); | ||
| const estimate = estimateModelCost(model, tokens, tokens); | ||
| jsonResponse(res, { model, tokens, estimate }); | ||
| } else if (pathname === '/api/providers' && req.method === 'GET') { | ||
| const providers = PROVIDERS.map(p => ({ | ||
| name: p, | ||
| models: MODEL_CATALOG.filter(m => m.provider === p).length, | ||
| })); | ||
| } | ||
| // GET /api/cost | ||
| else if (pathname === '/api/cost' && req.method === 'GET') { | ||
| const model = parsedUrl.searchParams.get('model'); | ||
| if (model) { | ||
| const tokens = parseInt(parsedUrl.searchParams.get('tokens') || '1000', 10); | ||
| const estimate = estimateModelCost(model, tokens, tokens); | ||
| jsonResponse(res, { model, tokens, estimate }); | ||
| } else { | ||
| jsonResponse(res, { | ||
| today: usageStats.today, | ||
| week: usageStats.week, | ||
| month: usageStats.month, | ||
| byModel: usageStats.byModel, | ||
| budget: budgetLimit, | ||
| }); | ||
| } | ||
| } | ||
| // GET /api/cost/trend | ||
| else if (pathname === '/api/cost/trend' && req.method === 'GET') { | ||
| const trend = []; | ||
| const now = new Date(); | ||
| for (let i = 29; i >= 0; i--) { | ||
| const d = new Date(now); | ||
| d.setDate(d.getDate() - i); | ||
| const dateStr = d.toISOString().slice(0, 10); | ||
| const tokens = Math.floor(30000 + Math.random() * 50000); | ||
| const cost = parseFloat((tokens * 0.000003).toFixed(4)); | ||
| trend.push({ date: dateStr, tokens, cost }); | ||
| } | ||
| jsonResponse(res, { trend }); | ||
| } | ||
| // PUT /api/cost/budget | ||
| else if (pathname === '/api/cost/budget' && req.method === 'PUT') { | ||
| const body = JSON.parse(await readBody(req)); | ||
| if (typeof body.budget === 'number' && body.budget > 0) { | ||
| budgetLimit = body.budget; | ||
| jsonResponse(res, { budget: budgetLimit, message: 'Budget updated' }); | ||
| } else { | ||
| jsonResponse(res, { error: 'Invalid budget value' }, 400); | ||
| } | ||
| } | ||
| // GET /api/providers | ||
| else if (pathname === '/api/providers' && req.method === 'GET') { | ||
| const providers = PROVIDER_META.map(p => { | ||
| const models = getProviderModels(p.name); | ||
| const llmModels = models.filter(m => m.type === 'llm'); | ||
| const embModels = models.filter(m => m.type === 'embedding'); | ||
| const prices = models.filter(m => m.inputPricePer1M > 0).map(m => m.inputPricePer1M); | ||
| return { | ||
| ...p, | ||
| models: models.length, | ||
| llmCount: llmModels.length, | ||
| embeddingCount: embModels.length, | ||
| priceRange: prices.length > 0 | ||
| ? { min: Math.min(...prices), max: Math.max(...prices), currency: models[0]?.currency || 'USD' } | ||
| : { min: 0, max: 0, currency: 'USD' }, | ||
| }; | ||
| }); | ||
| jsonResponse(res, { providers }); | ||
| } else if (pathname === '/api/providers/check' && req.method === 'GET') { | ||
| } | ||
| // GET /api/providers/check | ||
| else if (pathname === '/api/providers/check' && req.method === 'GET') { | ||
| const name = parsedUrl.searchParams.get('name') || ''; | ||
| const result = await checkProvider(name); | ||
| jsonResponse(res, { provider: name, ...result }); | ||
| } else if (pathname === '/api/brain/status' && req.method === 'GET') { | ||
| } | ||
| // GET /api/providers/:name | ||
| else if (pathname.match(/^\/api\/providers\/[^/]+$/) && !pathname.includes('/models') && !pathname.includes('/check') && req.method === 'GET') { | ||
| const name = pathname.split('/')[3]; | ||
| const meta = getProviderMeta(name); | ||
| if (!meta) { | ||
| jsonResponse(res, { error: 'Provider not found' }, 404); | ||
| } else { | ||
| const models = getProviderModels(name); | ||
| jsonResponse(res, { provider: meta, models }); | ||
| } | ||
| } | ||
| // GET /api/providers/:name/models | ||
| else if (pathname.match(/^\/api\/providers\/[^/]+\/models$/) && req.method === 'GET') { | ||
| const name = pathname.split('/')[3]; | ||
| const models = getProviderModels(name); | ||
| jsonResponse(res, { models }); | ||
| } | ||
| // GET /api/keys | ||
| else if (pathname === '/api/keys' && req.method === 'GET') { | ||
| const keys: Array<{ provider: string; masked: string; addedAt: string; status: string }> = []; | ||
| // Check env vars | ||
| const envKeyMap: Record<string, string> = { | ||
| openai: 'OPENAI_API_KEY', anthropic: 'ANTHROPIC_API_KEY', gemini: 'GEMINI_API_KEY', | ||
| deepseek: 'DEEPSEEK_API_KEY', moonshot: 'MOONSHOT_API_KEY', zhipu: 'ZHIPU_API_KEY', | ||
| dashscope: 'DASHSCOPE_API_KEY', minimax: 'MINIMAX_API_KEY', yi: 'YI_API_KEY', | ||
| baichuan: 'BAICHUAN_API_KEY', siliconflow: 'SILICONFLOW_API_KEY', stepfun: 'STEPFUN_API_KEY', | ||
| fireworks: 'FIREWORKS_API_KEY', together: 'TOGETHER_API_KEY', groq: 'GROQ_API_KEY', | ||
| perplexity: 'PPLX_API_KEY', cohere: 'COHERE_API_KEY', jina: 'JINA_API_KEY', | ||
| voyage: 'VOYAGE_API_KEY', grok: 'GROK_API_KEY', | ||
| }; | ||
| for (const [prov, envName] of Object.entries(envKeyMap)) { | ||
| const envVal = process.env[envName]; | ||
| if (envVal) { | ||
| keys.push({ provider: prov, masked: maskKey(envVal), addedAt: 'env', status: 'configured' }); | ||
| } | ||
| } | ||
| // Session keys | ||
| for (const [prov, data] of apiKeys.entries()) { | ||
| keys.push({ provider: prov, masked: maskKey(data.key), addedAt: data.addedAt, status: 'session' }); | ||
| } | ||
| jsonResponse(res, { keys }); | ||
| } | ||
| // POST /api/keys | ||
| else if (pathname === '/api/keys' && req.method === 'POST') { | ||
| const body = JSON.parse(await readBody(req)); | ||
| if (!body.provider || !body.key) { | ||
| jsonResponse(res, { error: 'provider and key are required' }, 400); | ||
| } else { | ||
| apiKeys.set(body.provider, { key: body.key, addedAt: new Date().toISOString() }); | ||
| jsonResponse(res, { message: 'Key added', provider: body.provider }); | ||
| } | ||
| } | ||
| // DELETE /api/keys/:provider | ||
| else if (pathname.match(/^\/api\/keys\/[^/]+$/) && req.method === 'DELETE') { | ||
| const provider = pathname.split('/')[3]; | ||
| const deleted = apiKeys.delete(provider); | ||
| jsonResponse(res, { deleted, provider }); | ||
| } | ||
| // POST /api/keys/test | ||
| else if (pathname === '/api/keys/test' && req.method === 'POST') { | ||
| const body = JSON.parse(await readBody(req)); | ||
| const provider = body.provider || ''; | ||
| const result = await checkProvider(provider); | ||
| jsonResponse(res, { provider, ...result }); | ||
| } | ||
| // POST /api/keys/test-all | ||
| else if (pathname === '/api/keys/test-all' && req.method === 'POST') { | ||
| const allProviders = [...new Set(MODEL_CATALOG.map(m => m.provider))]; | ||
| const results: Array<{ provider: string; available: boolean; latencyMs: number; error?: string }> = []; | ||
| for (const p of allProviders) { | ||
| try { | ||
| const r = await checkProvider(p); | ||
| results.push({ provider: p, ...r }); | ||
| } catch (e: any) { | ||
| results.push({ provider: p, available: false, latencyMs: 0, error: e.message }); | ||
| } | ||
| } | ||
| jsonResponse(res, { results }); | ||
| } | ||
| // GET /api/brain/status | ||
| else if (pathname === '/api/brain/status' && req.method === 'GET') { | ||
| jsonResponse(res, { | ||
@@ -135,3 +400,5 @@ available: true, | ||
| }); | ||
| } else if (pathname === '/api/test' && req.method === 'POST') { | ||
| } | ||
| // POST /api/test | ||
| else if (pathname === '/api/test' && req.method === 'POST') { | ||
| const body = JSON.parse(await readBody(req)); | ||
@@ -144,4 +411,5 @@ jsonResponse(res, { | ||
| }); | ||
| } else if (pathname === '/' || pathname === '/index.html') { | ||
| // Serve HTML | ||
| } | ||
| // Serve index.html for all non-API routes (SPA) | ||
| else if (!pathname.startsWith('/api/')) { | ||
| const htmlPath = path.join(this.config.staticDir, 'index.html'); | ||
@@ -156,3 +424,4 @@ try { | ||
| } | ||
| } else { | ||
| } | ||
| else { | ||
| jsonResponse(res, { error: 'Not found' }, 404); | ||
@@ -159,0 +428,0 @@ } |
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
2523934
3.01%19662
3.42%530
29.27%75
2.74%73
30.36%