diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 584808009c..2a6babbdab 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -2827,6 +2827,8 @@ export async function handleChatCore({ translatedBody.messages?.length || translatedBody.contents?.length || translatedBody.request?.contents?.length || + (translatedBody.conversationState?.history?.length ?? 0) + + (translatedBody.conversationState?.currentMessage ? 1 : 0) || 0; log?.debug?.("REQUEST", `${provider.toUpperCase()} | ${model} | ${msgCount} msgs`); diff --git a/src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx b/src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx index d02a86b9d4..14473409f9 100644 --- a/src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx +++ b/src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx @@ -304,18 +304,20 @@ export default function CostOverviewTab() { requestedModelCoveragePct: 0, streak: 0, }; + const hasCostData = summary.totalCost > 0; + const providersByCost = [...(analytics?.byProvider || [])] - .filter((provider) => provider.cost > 0) - .sort((left, right) => right.cost - left.cost); + .filter((provider) => (hasCostData ? provider.cost > 0 : provider.requests > 0)) + .sort((left, right) => (hasCostData ? right.cost - left.cost : right.requests - left.requests)); const modelsByCost = [...(analytics?.byModel || [])] - .filter((model) => model.cost > 0) - .sort((left, right) => right.cost - left.cost); + .filter((model) => (hasCostData ? model.cost > 0 : model.requests > 0)) + .sort((left, right) => (hasCostData ? right.cost - left.cost : right.requests - left.requests)); const apiKeysByCost = [...(analytics?.byApiKey || [])] - .filter((apiKey) => apiKey.cost > 0) - .sort((left, right) => right.cost - left.cost); + .filter((apiKey) => (hasCostData ? apiKey.cost > 0 : apiKey.requests > 0)) + .sort((left, right) => (hasCostData ? right.cost - left.cost : right.requests - left.requests)); const accountsByCost = [...(analytics?.byAccount || [])] - .filter((account) => account.cost > 0) - .sort((left, right) => right.cost - left.cost); + .filter((account) => (hasCostData ? account.cost > 0 : account.requests > 0)) + .sort((left, right) => (hasCostData ? right.cost - left.cost : right.requests - left.requests)); const avgCostPerRequest = summary.totalRequests > 0 ? summary.totalCost / summary.totalRequests : 0; const dailyTrend = analytics?.dailyTrend || []; @@ -642,7 +644,7 @@ export default function CostOverviewTab() { )} - {summary.totalCost <= 0 ? ( + {summary.totalCost <= 0 && summary.totalRequests <= 0 ? ( ) : ( <> -
- - -
+ {hasCostData && ( +
+ + +
+ )}
@@ -1047,14 +1057,16 @@ function TopListCard({ secondaryKey, secondaryLabel, locale, + hasCostData, }: { title: string; - rows: Array>; + rows: Array>; nameKey: string; valueKey: string; secondaryKey?: string; secondaryLabel?: string; locale: string; + hasCostData?: boolean; }) { const currencyFormatter = createCurrencyFormatter(locale); @@ -1080,7 +1092,11 @@ function TopListCard({ ) : null} - {currencyFormatter.format(Number(row[valueKey] || 0))} + {hasCostData || Number(row[valueKey] || 0) > 0 ? ( + currencyFormatter.format(Number(row[valueKey] || 0)) + ) : ( + Legacy / Free + )} @@ -1104,7 +1120,7 @@ function CostBreakdownTable({ locale, }: { title: string; - rows: Array>; + rows: Array>; columns: ColumnDef[]; locale: string; }) { @@ -1114,7 +1130,7 @@ function CostBreakdownTable({ const num = Number(value || 0); switch (format) { case "currency": - return currencyFormatter.format(num); + return num > 0 ? currencyFormatter.format(num) : "Legacy / Free"; case "compact": return new Intl.NumberFormat(locale, { notation: "compact" }).format(num); case "number": diff --git a/src/app/api/settings/export-json/route.ts b/src/app/api/settings/export-json/route.ts index 482a98b9a9..6eaa783728 100644 --- a/src/app/api/settings/export-json/route.ts +++ b/src/app/api/settings/export-json/route.ts @@ -6,6 +6,7 @@ import { getCombos, getApiKeys, } from "@/lib/localDb"; +import { getDbInstance } from "@/lib/db/core"; import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth"; /** @@ -32,12 +33,20 @@ export async function GET(request: Request) { const combos = await getCombos(); const apiKeys = await getApiKeys(); + const db = getDbInstance(); + const usageHistory = db.prepare("SELECT * FROM usage_history").all(); + const domainCostHistory = db.prepare("SELECT * FROM domain_cost_history").all(); + const domainBudgets = db.prepare("SELECT * FROM domain_budgets").all(); + const exportData = { settings: safeSettings, providerConnections, providerNodes, combos, apiKeys, + usageHistory, + domainCostHistory, + domainBudgets, // Metadata to identify export version _meta: { exportedAt: new Date().toISOString(), diff --git a/src/app/api/settings/import-json/route.ts b/src/app/api/settings/import-json/route.ts index 650336a313..0d7b4ebf67 100644 --- a/src/app/api/settings/import-json/route.ts +++ b/src/app/api/settings/import-json/route.ts @@ -67,7 +67,9 @@ export async function POST(request: Request) { console.log( `[JSON Import] Imported ${counts.connections} connections, ${counts.nodes} nodes, ` + - `${counts.combos} combos, ${counts.apiKeys} API keys` + `${counts.combos} combos, ${counts.apiKeys} API keys, ` + + `${counts.usageHistory} usage rows, ${counts.domainCostHistory} cost rows, ` + + `${counts.domainBudgets} budgets` ); return NextResponse.json({ diff --git a/src/lib/db/jsonMigration.ts b/src/lib/db/jsonMigration.ts index 5e6e237485..1251b8b60d 100644 --- a/src/lib/db/jsonMigration.ts +++ b/src/lib/db/jsonMigration.ts @@ -32,6 +32,9 @@ export interface LegacyJsonData { combos?: unknown; keys?: unknown; }; + usageHistory?: Record[]; + domainCostHistory?: Record[]; + domainBudgets?: Record[]; } /** @@ -43,7 +46,15 @@ export interface LegacyJsonData { export function runJsonMigration( db: SqliteDatabase, data: LegacyJsonData -): { connections: number; nodes: number; combos: number; apiKeys: number } { +): { + connections: number; + nodes: number; + combos: number; + apiKeys: number; + usageHistory: number; + domainCostHistory: number; + domainBudgets: number; +} { const insertConn = db.prepare(` INSERT OR REPLACE INTO provider_connections ( id, provider, auth_type, name, email, priority, is_active, @@ -210,6 +221,89 @@ export function runJsonMigration( createdAt: apiKey.createdAt ?? new Date().toISOString(), }); } + // 7. Usage History + if (data.usageHistory && data.usageHistory.length > 0) { + const insertUsageHistory = db.prepare(` + INSERT OR REPLACE INTO usage_history ( + id, provider, model, connection_id, api_key_id, api_key_name, + tokens_input, tokens_output, tokens_cache_read, tokens_cache_creation, + tokens_reasoning, status, success, latency_ms, ttft_ms, error_code, timestamp + ) VALUES ( + @id, @provider, @model, @connection_id, @api_key_id, @api_key_name, + @tokens_input, @tokens_output, @tokens_cache_read, @tokens_cache_creation, + @tokens_reasoning, @status, @success, @latency_ms, @ttft_ms, @error_code, @timestamp + ) + `); + for (const row of data.usageHistory) { + insertUsageHistory.run({ + id: row.id, + provider: row.provider ?? null, + model: row.model ?? null, + connection_id: row.connection_id ?? null, + api_key_id: row.api_key_id ?? null, + api_key_name: row.api_key_name ?? null, + tokens_input: row.tokens_input ?? 0, + tokens_output: row.tokens_output ?? 0, + tokens_cache_read: row.tokens_cache_read ?? 0, + tokens_cache_creation: row.tokens_cache_creation ?? 0, + tokens_reasoning: row.tokens_reasoning ?? 0, + status: row.status ?? null, + success: row.success ?? 1, + latency_ms: row.latency_ms ?? 0, + ttft_ms: row.ttft_ms ?? 0, + error_code: row.error_code ?? null, + timestamp: row.timestamp, + }); + } + } + + // 8. Domain Cost History + if (data.domainCostHistory && data.domainCostHistory.length > 0) { + const insertCostHistory = db.prepare(` + INSERT OR REPLACE INTO domain_cost_history ( + id, api_key_id, cost, timestamp + ) VALUES ( + @id, @api_key_id, @cost, @timestamp + ) + `); + for (const row of data.domainCostHistory) { + insertCostHistory.run({ + id: row.id, + api_key_id: row.api_key_id, + cost: row.cost, + timestamp: row.timestamp, + }); + } + } + // 9. Domain Budgets + if (data.domainBudgets && data.domainBudgets.length > 0) { + const insertBudgets = db.prepare(` + INSERT OR REPLACE INTO domain_budgets ( + api_key_id, daily_limit_usd, weekly_limit_usd, monthly_limit_usd, + warning_threshold, reset_interval, reset_time, budget_reset_at, + last_budget_reset_at, warning_emitted_at, warning_period_start + ) VALUES ( + @api_key_id, @daily_limit_usd, @weekly_limit_usd, @monthly_limit_usd, + @warning_threshold, @reset_interval, @reset_time, @budget_reset_at, + @last_budget_reset_at, @warning_emitted_at, @warning_period_start + ) + `); + for (const row of data.domainBudgets) { + insertBudgets.run({ + api_key_id: row.api_key_id, + daily_limit_usd: row.daily_limit_usd, + weekly_limit_usd: row.weekly_limit_usd ?? 0, + monthly_limit_usd: row.monthly_limit_usd ?? 0, + warning_threshold: row.warning_threshold ?? 0.8, + reset_interval: row.reset_interval ?? "daily", + reset_time: row.reset_time ?? "00:00", + budget_reset_at: row.budget_reset_at ?? null, + last_budget_reset_at: row.last_budget_reset_at ?? null, + warning_emitted_at: row.warning_emitted_at ?? null, + warning_period_start: row.warning_period_start ?? null, + }); + } + } }); migrate(); @@ -219,5 +313,8 @@ export function runJsonMigration( nodes: (data.providerNodes ?? []).length, combos: (data.combos ?? []).length, apiKeys: (data.apiKeys ?? []).length, + usageHistory: (data.usageHistory ?? []).length, + domainCostHistory: (data.domainCostHistory ?? []).length, + domainBudgets: (data.domainBudgets ?? []).length, }; } diff --git a/test-kiro.ts b/test-kiro.ts new file mode 100644 index 0000000000..943462dbd2 --- /dev/null +++ b/test-kiro.ts @@ -0,0 +1,17 @@ +import { buildKiroPayload } from "./open-sse/translator/request/openai-to-kiro.js"; + +const messages = []; +for (let i = 0; i < 2; i++) { + messages.push({ role: "user", content: `user ${i}` }); + messages.push({ role: "assistant", content: `assistant ${i}` }); +} +messages.push({ role: "user", content: "use tool" }); +messages.push({ + role: "assistant", + tool_calls: [{ id: "call_1", function: { name: "test", arguments: "{}" } }], +}); +messages.push({ role: "tool", tool_call_id: "call_1", content: "ok" }); +messages.push({ role: "user", content: "last user" }); + +const payload = buildKiroPayload("kr/claude-sonnet-4.5", { messages, tools: [] }, false, {}); +console.log(JSON.stringify(payload, null, 2)); diff --git a/tests/e2e/combo-unification.spec.ts b/tests/e2e/combo-unification.spec.ts index 1a767a833a..ab84041f09 100644 --- a/tests/e2e/combo-unification.spec.ts +++ b/tests/e2e/combo-unification.spec.ts @@ -162,7 +162,7 @@ test.describe("Combo Unification", () => { await page.waitForLoadState("networkidle"); const sidebar = page.locator("aside, nav").first(); - await expect(sidebar.getByText("Combos")).toBeVisible(); + await expect(sidebar.getByText("Combos", { exact: true })).toBeVisible(); await expect(sidebar.getByText("Auto Combo")).toHaveCount(0); }); diff --git a/tests/e2e/proxy-registry.smoke.spec.ts b/tests/e2e/proxy-registry.smoke.spec.ts index 9ef481153a..f7f45c8218 100644 --- a/tests/e2e/proxy-registry.smoke.spec.ts +++ b/tests/e2e/proxy-registry.smoke.spec.ts @@ -172,8 +172,7 @@ test.describe("Proxy Registry smoke flow", () => { }); }); - await gotoDashboardRoute(page, "/dashboard/settings?tab=advanced"); - await page.waitForLoadState("networkidle"); + await gotoDashboardRoute(page, "/dashboard/system/proxy"); await expect(page.getByRole("heading", { name: "Proxy Registry" })).toBeVisible(); diff --git a/tests/e2e/settings-toggles.spec.ts b/tests/e2e/settings-toggles.spec.ts index 8f05a14cad..7988e16e68 100644 --- a/tests/e2e/settings-toggles.spec.ts +++ b/tests/e2e/settings-toggles.spec.ts @@ -30,9 +30,7 @@ test.describe("Settings Toggles", () => { ); test("Debug mode toggle should work", async ({ page }) => { - await gotoDashboardRoute(page, "/dashboard/settings"); - await waitForSettingsShell(page); - await page.getByRole("tab", { name: /advanced/i }).click(); + await gotoDashboardRoute(page, "/dashboard/system/proxy"); const debugToggle = getDebugToggle(page); @@ -99,9 +97,7 @@ test.describe("Settings Toggles", () => { }); test("Debug mode should persist after page reload", async ({ page }) => { - await gotoDashboardRoute(page, "/dashboard/settings"); - await waitForSettingsShell(page); - await page.getByRole("tab", { name: /advanced/i }).click(); + await gotoDashboardRoute(page, "/dashboard/system/proxy"); const debugToggle = getDebugToggle(page); @@ -113,8 +109,7 @@ test.describe("Settings Toggles", () => { const nextState = initialState === "true" ? "false" : "true"; await expect(debugToggle).toHaveAttribute("aria-checked", nextState, { timeout: 15000 }); await page.reload(); - await waitForSettingsShell(page); - await page.getByRole("tab", { name: /advanced/i }).click(); + await page.waitForLoadState("domcontentloaded"); const reloadedToggle = getDebugToggle(page); await expect(reloadedToggle).toBeEnabled({ timeout: 15000 }); await expect(reloadedToggle).toHaveAttribute("aria-checked", nextState, {