Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement โ†’
Sign In

agentkits

Package Overview
Dependencies
Maintainers
1
Versions
36
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

agentkits - npm Package Compare versions

Comparing version
2.0.0
to
2.0.1
+19
-0
dist/ui/server.d.ts

@@ -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 {

@@ -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 @@ [![npm version](https://img.shields.io/npm/v/agentkits.svg?style=flat-square)](https://www.npmjs.com/package/agentkits)

[![TypeScript](https://img.shields.io/badge/TypeScript-5.7+-blue.svg?style=flat-square)](https://www.typescriptlang.org/)
[![Tests](https://img.shields.io/badge/tests-571%20passing-brightgreen.svg?style=flat-square)](#)
[![Tests](https://img.shields.io/badge/tests-687%20passing-brightgreen.svg?style=flat-square)](#)
[![Node.js](https://img.shields.io/badge/node-%3E%3D18-green.svg?style=flat-square)](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 @@

<!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>

@@ -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 @@ }