diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 0f15ae1..2d52afd 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -1,7 +1,23 @@ import { NextRequest } from "next/server"; +import { getDb } from "@/lib/db/schema"; import { getNextApiKey } from "@/lib/api-keys"; import { PROVIDER_URLS } from "@/lib/providers"; +function estimateTokens(messages: Array<{ role: string; content: string }>): number { + return Math.ceil(JSON.stringify(messages).length / 3); +} + +function trackTokenUsage(provider: string, modelId: string, inputTokens: number, outputTokens: number) { + try { + const db = getDb(); + db.prepare( + "INSERT INTO token_usage (provider, model_id, input_tokens, output_tokens, estimated_cost_usd) VALUES (?, ?, ?, ?, 0)" + ).run(provider, modelId, inputTokens, outputTokens); + } catch { + // non-critical + } +} + export const dynamic = "force-dynamic"; export async function POST(req: NextRequest) { @@ -34,6 +50,7 @@ export async function POST(req: NextRequest) { // Simple messages format — only role + content string const cleanMessages = messages.map(m => ({ role: m.role, content: m.content })); + const estimatedInputTokens = estimateTokens(cleanMessages); const res = await fetch(url, { method: "POST", @@ -62,6 +79,7 @@ export async function POST(req: NextRequest) { async start(controller) { const decoder = new TextDecoder(); let buffer = ""; + let outputChars = 0; try { while (true) { const { done, value } = await reader.read(); @@ -79,6 +97,7 @@ export async function POST(req: NextRequest) { const json = JSON.parse(data); const content = json.choices?.[0]?.delta?.content; if (content) { + outputChars += content.length; controller.enqueue(new TextEncoder().encode(content)); } } catch { /* skip */ } @@ -87,6 +106,7 @@ export async function POST(req: NextRequest) { } catch (err) { console.error("[chat] stream error:", err); } finally { + trackTokenUsage(provider, modelId, estimatedInputTokens, Math.ceil(outputChars / 3)); controller.close(); } }, diff --git a/src/app/api/models/route.ts b/src/app/api/models/route.ts index b23f0d0..5dc7be4 100644 --- a/src/app/api/models/route.ts +++ b/src/app/api/models/route.ts @@ -32,6 +32,8 @@ export async function GET(req: NextRequest) { m.context_length as contextLength, m.tier, m.nickname, + m.supports_tools as supportsTools, + m.supports_vision as supportsVision, m.first_seen as firstSeen, m.last_seen as lastSeen, h.status as healthStatus, @@ -76,6 +78,8 @@ export async function GET(req: NextRequest) { contextLength: number; tier: string; nickname: string | null; + supportsTools: number | null; + supportsVision: number | null; firstSeen: string; lastSeen: string; healthStatus: string | null; @@ -106,6 +110,8 @@ export async function GET(req: NextRequest) { modelId: r.modelId, contextLength: r.contextLength, tier: r.tier, + supportsTools: r.supportsTools === 1 ? true : r.supportsTools === 0 ? false : null, + supportsVision: r.supportsVision === 1 ? true : r.supportsVision === 0 ? false : null, health: { status: healthStatusFinal, latencyMs: r.latencyMs ?? 0, diff --git a/src/app/api/status/route.ts b/src/app/api/status/route.ts index e3281cc..434f87c 100644 --- a/src/app/api/status/route.ts +++ b/src/app/api/status/route.ts @@ -1,9 +1,12 @@ import { NextResponse } from "next/server"; import { getDb } from "@/lib/db/schema"; import { getCached, setCache } from "@/lib/cache"; +import { ensureWorkerStarted } from "@/lib/worker/startup"; export const dynamic = "force-dynamic"; +ensureWorkerStarted(); + export async function GET() { try { const cached = getCached("api:status"); diff --git a/src/app/page.tsx b/src/app/page.tsx index 8988e79..96cce9f 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -176,6 +176,25 @@ function GatewayConfigCard() { ); } +function fmtUsd(amount: number) { + if (amount === 0) return "$0.00"; + if (amount < 0.01) return `$${amount.toFixed(4)}`; + if (amount < 1) return `$${amount.toFixed(3)}`; + return `$${amount.toFixed(2)}`; +} + +function fmtThb(amount: number) { + if (amount === 0) return "฿0"; + if (amount < 1) return `฿${amount.toFixed(2)}`; + return `฿${amount.toFixed(0)}`; +} + +function fmtTokenSummary(tokens: number) { + if (tokens <= 0) return "0 tokens"; + if (tokens < 1000) return `${tokens} tokens`; + return `${(tokens / 1000).toFixed(1)}K tokens`; +} + // ─── Main Dashboard ──────────────────────────────────────────────────────────── export default function Dashboard() { @@ -478,14 +497,14 @@ export default function Dashboard() { {/* Cost Savings Card */} - {costSavings && costSavings.totalTokens > 0 && ( + {costSavings && (
-
+
💰 เทียบต้นทุน
-
+
สะสม {costSavings.totalRequests.toLocaleString()} requests | วันนี้ {costSavings.todayRequests.toLocaleString()} @@ -493,43 +512,51 @@ export default function Dashboard() {
{/* Token usage summary */} -
+
ใช้ไป: - {(costSavings.totalTokens / 1000).toFixed(0)}K tokens - (input {(costSavings.totalInputTokens / 1000).toFixed(0)}K + output {(costSavings.totalOutputTokens / 1000).toFixed(0)}K) + {fmtTokenSummary(costSavings.totalTokens)} + (input {fmtTokenSummary(costSavings.totalInputTokens)} + output {fmtTokenSummary(costSavings.totalOutputTokens)})
- {/* Cost comparison — dynamic from API */} -
- {(costSavings.providers ?? []).map((p) => ( -
-
{p.label}
-
${p.cost.toFixed(2)}
-
฿{p.costThb.toFixed(0)}
-
${p.inputPrice}/${p.outputPrice} /1M
-
- ))} -
-
BCProxyAI
-
$0.00
-
ฟรี!
-
$0/$0 /1M
+ {costSavings.totalTokens === 0 ? ( +
+ ยังไม่มี usage ในฐานข้อมูลค่ะ — ตอนนี้ section นี้จะขึ้นเสมอ แม้ยังไม่มี token log
-
+ ) : ( + <> + {/* Cost comparison — dynamic from API */} +
+ {(costSavings.providers ?? []).map((p) => ( +
+
{p.label}
+
{fmtUsd(p.cost)}
+
{fmtThb(p.costThb)}
+
${p.inputPrice}/${p.outputPrice} /1M
+
+ ))} +
+
BCProxyAI
+
$0.00
+
ฟรี!
+
$0/$0 /1M
+
+
- {/* Total saved highlight — compare all */} -
-
ยอดสะสมที่ประหยัดได้ (เทียบแต่ละเจ้า):
-
- {(costSavings.providers ?? []).map((p) => ( -
-
vs {p.label}
-
${p.cost.toFixed(2)}
-
฿{p.costThb.toFixed(0)}
+ {/* Total saved highlight — compare all */} +
+
ยอดสะสมที่ประหยัดได้ (เทียบแต่ละเจ้า):
+
+ {(costSavings.providers ?? []).map((p) => ( +
+
vs {p.label}
+
{fmtUsd(p.cost)}
+
{fmtThb(p.costThb)}
+
+ ))}
- ))} -
-
+
+ + )}
)} diff --git a/src/components/ModelGrid.tsx b/src/components/ModelGrid.tsx index bc2c4f2..9980dac 100644 --- a/src/components/ModelGrid.tsx +++ b/src/components/ModelGrid.tsx @@ -175,13 +175,35 @@ export function ModelGrid({ sortedModels, availableCount, cooldownCount, unknown
)} + {/* Capability badges */} +
+ + {model.supportsTools === true ? "tools ✓" : model.supportsTools === false ? "tools ✕" : "tools ?"} + + + {model.supportsVision === true ? "vision ✓" : model.supportsVision === false ? "vision ✕" : "vision ?"} + +
+ {/* Fun status line */} -
- +
+ {funStatus.emoji} {funStatus.text} {gradeInfo && ( - + {gradeInfo.emoji} {gradeInfo.label} )} diff --git a/src/components/shared.tsx b/src/components/shared.tsx index e4ace89..7d0ad5d 100644 --- a/src/components/shared.tsx +++ b/src/components/shared.tsx @@ -67,6 +67,8 @@ export interface ModelData { modelId: string; contextLength: number; tier: string; + supportsTools: boolean | null; + supportsVision: boolean | null; health: HealthInfo; benchmark: BenchmarkInfo | null; firstSeen: string;