swayrouter 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +43 -0
- package/CHANGELOG.md +132 -0
- package/LICENSE +30 -0
- package/README.id.md +633 -0
- package/README.md +628 -0
- package/cli/cli.js +388 -0
- package/cli/package.json +4 -0
- package/cli/src/cli/commands/xaiVideo.js +280 -0
- package/cli/src/cli/tray/SwayAvatar.svg +20 -0
- package/cli/src/cli/tray/autostart.js +351 -0
- package/cli/src/cli/tray/icon.ico +0 -0
- package/cli/src/cli/tray/icon.png +0 -0
- package/cli/src/cli/tray/tray.js +258 -0
- package/cli/src/cli/tray/tray.ps1 +129 -0
- package/cli/src/cli/tray/trayHost.js +157 -0
- package/cli/src/cli/tray/trayWin.js +83 -0
- package/cli/src/cli/utils/display.js +123 -0
- package/cli/src/cli/utils/input.js +138 -0
- package/cli/src/runtime/discover.js +153 -0
- package/cli/src/runtime/lifecycle.js +333 -0
- package/cli/src/runtime/runMenu.js +250 -0
- package/cli/src/runtime/uiAdapters.js +12 -0
- package/cli/src/runtime/update.js +130 -0
- package/dashboard/dist/assets/CliTools-Ch3gKuNw.js +24 -0
- package/dashboard/dist/assets/Combo-BBCldjWi.js +1 -0
- package/dashboard/dist/assets/ConsoleLog-wywAYcPT.js +6 -0
- package/dashboard/dist/assets/Dashboard-DxzyF41b.js +1 -0
- package/dashboard/dist/assets/Header-C_9Su9BF.js +1 -0
- package/dashboard/dist/assets/Keys-D0ufgcsc.js +1 -0
- package/dashboard/dist/assets/Login-BeKpipS_.js +1 -0
- package/dashboard/dist/assets/ManageApiKey-CVXsmW2B.js +1 -0
- package/dashboard/dist/assets/OAuthCallback-D_UZYcF6.js +1 -0
- package/dashboard/dist/assets/Provider-BU7JfTqm.js +4 -0
- package/dashboard/dist/assets/ProviderModelAccordion-BAifkfVR.js +1 -0
- package/dashboard/dist/assets/Proxy-CU15VJ--.js +1 -0
- package/dashboard/dist/assets/QuotaMonitor-HzGK7bcc.js +1 -0
- package/dashboard/dist/assets/Settings-Dt4y2dC0.js +1 -0
- package/dashboard/dist/assets/Setup-DSDUwU7N.js +1 -0
- package/dashboard/dist/assets/StatusBadge-1nAOyHU2.js +1 -0
- package/dashboard/dist/assets/SwayChat-Bup7up6D.js +10 -0
- package/dashboard/dist/assets/Usage-eWnrPQ_v.js +4 -0
- package/dashboard/dist/assets/accordion-DOOPDDgt.js +1 -0
- package/dashboard/dist/assets/admin-extras-api-CCdu5TFM.js +1 -0
- package/dashboard/dist/assets/api-keys-api-ifAC2Ok3.js +1 -0
- package/dashboard/dist/assets/card-CC0Omjcv.js +1 -0
- package/dashboard/dist/assets/chatStudio-C0zPz_Z_.js +2 -0
- package/dashboard/dist/assets/checkbox-7SZBrmbN.js +1 -0
- package/dashboard/dist/assets/connections-api-_N54UD3S.js +1 -0
- package/dashboard/dist/assets/copy-button-qBvM6H9z.js +1 -0
- package/dashboard/dist/assets/currency-api-SV9CBf5e.js +1 -0
- package/dashboard/dist/assets/flip-button-BGgkjtl0.js +1 -0
- package/dashboard/dist/assets/frame-cUiti6Lq.js +1 -0
- package/dashboard/dist/assets/icons-vendor-CUx7SB5V.js +541 -0
- package/dashboard/dist/assets/index-B6j-9zHy.js +10 -0
- package/dashboard/dist/assets/index-C-_l0yYz.css +1 -0
- package/dashboard/dist/assets/live-mode-CHShLY4E.js +1 -0
- package/dashboard/dist/assets/markdown-vendor-BY8e1gQ8.js +29 -0
- package/dashboard/dist/assets/motion-vendor-g5fpGzmJ.js +9 -0
- package/dashboard/dist/assets/oauth-callback-CqYQzyd7.js +1 -0
- package/dashboard/dist/assets/query-vendor-DVevaSVS.js +1 -0
- package/dashboard/dist/assets/react-vendor-xnYrwzsL.js +61 -0
- package/dashboard/dist/assets/ripple-button-l8aMiVVG.js +1 -0
- package/dashboard/dist/assets/segmented-CqmEnQjZ.js +1 -0
- package/dashboard/dist/assets/select-D8pUtMXp.js +1 -0
- package/dashboard/dist/assets/skeleton-7uJDiIXe.js +1 -0
- package/dashboard/dist/assets/table-D0M60Tu1.js +1 -0
- package/dashboard/dist/assets/tabs-C6YkuulG.js +1 -0
- package/dashboard/dist/assets/tunnel-api-DDPsBaDV.js +1 -0
- package/dashboard/dist/assets/ui-vendor-BuM9f2NY.js +17 -0
- package/dashboard/dist/image2/SwayAvatar.svg +20 -0
- package/dashboard/dist/index.html +32 -0
- package/dashboard/dist/logo/avatar.svg +1 -0
- package/dashboard/dist/logo/sway.svg +20 -0
- package/dashboard/dist/logo/swaychat.svg +20 -0
- package/dashboard/dist/providers/SwayCustom.svg +19 -0
- package/dashboard/dist/providers/alicode.svg +12 -0
- package/dashboard/dist/providers/anthropic.svg +11 -0
- package/dashboard/dist/providers/antigravity.svg +3 -0
- package/dashboard/dist/providers/azure.svg +5 -0
- package/dashboard/dist/providers/baidu.svg +3 -0
- package/dashboard/dist/providers/blackbox.svg +5 -0
- package/dashboard/dist/providers/byteplus.svg +3 -0
- package/dashboard/dist/providers/cerebras.svg +4 -0
- package/dashboard/dist/providers/chutes.svg +4 -0
- package/dashboard/dist/providers/claude.svg +3 -0
- package/dashboard/dist/providers/cline.svg +11 -0
- package/dashboard/dist/providers/cloudflare-ai.svg +4 -0
- package/dashboard/dist/providers/codebuddy.svg +11 -0
- package/dashboard/dist/providers/codex.svg +10 -0
- package/dashboard/dist/providers/commandcode.svg +3 -0
- package/dashboard/dist/providers/cursor.svg +3 -0
- package/dashboard/dist/providers/deepseek.svg +10 -0
- package/dashboard/dist/providers/featherless.svg +3 -0
- package/dashboard/dist/providers/fireworks.svg +3 -0
- package/dashboard/dist/providers/gemini.svg +3 -0
- package/dashboard/dist/providers/geminicli.svg +11 -0
- package/dashboard/dist/providers/github.svg +3 -0
- package/dashboard/dist/providers/grok.svg +3 -0
- package/dashboard/dist/providers/groq.svg +3 -0
- package/dashboard/dist/providers/hermesagent.svg +1 -0
- package/dashboard/dist/providers/kilocode.svg +15 -0
- package/dashboard/dist/providers/kimchi.svg +3 -0
- package/dashboard/dist/providers/kimi.svg +4 -0
- package/dashboard/dist/providers/kiro.svg +10 -0
- package/dashboard/dist/providers/meta.svg +3 -0
- package/dashboard/dist/providers/mimo.svg +10 -0
- package/dashboard/dist/providers/minimax.svg +3 -0
- package/dashboard/dist/providers/mistral.svg +3 -0
- package/dashboard/dist/providers/morph.svg +3 -0
- package/dashboard/dist/providers/nebius.svg +4 -0
- package/dashboard/dist/providers/nvidia.svg +3 -0
- package/dashboard/dist/providers/ollama.svg +3 -0
- package/dashboard/dist/providers/omp.svg +5 -0
- package/dashboard/dist/providers/openai.svg +3 -0
- package/dashboard/dist/providers/opencode.svg +3 -0
- package/dashboard/dist/providers/openrouter.svg +3 -0
- package/dashboard/dist/providers/perplexity.svg +3 -0
- package/dashboard/dist/providers/poolside.svg +36 -0
- package/dashboard/dist/providers/qoder.svg +11 -0
- package/dashboard/dist/providers/sumopod.svg +3 -0
- package/dashboard/dist/providers/tencent.svg +5 -0
- package/dashboard/dist/providers/together.svg +12 -0
- package/dashboard/dist/providers/trae.svg +3 -0
- package/dashboard/dist/providers/venice.svg +4 -0
- package/dashboard/dist/providers/vercel.svg +3 -0
- package/dashboard/dist/providers/vertex.svg +17 -0
- package/dashboard/dist/providers/windsurf.svg +10 -0
- package/dashboard/dist/providers/zai.svg +3 -0
- package/dashboard/tsconfig.json +13 -0
- package/package.json +87 -0
- package/src/config/appConstants.js +153 -0
- package/src/config/defaultThinkingSignature.js +7 -0
- package/src/config/errorConfig.js +117 -0
- package/src/config/grokCli.js +9 -0
- package/src/config/kiroConstants.js +235 -0
- package/src/config/mediaConfig.js +20 -0
- package/src/config/ollamaModels.js +18 -0
- package/src/config/providerModels.js +109 -0
- package/src/config/providers.js +17 -0
- package/src/config/runtimeConfig.js +84 -0
- package/src/dashboardGuard.js +254 -0
- package/src/executors/antigravity.js +389 -0
- package/src/executors/azure.js +49 -0
- package/src/executors/base.js +225 -0
- package/src/executors/codebuddy-cn.js +23 -0
- package/src/executors/codebuddy-intl.js +60 -0
- package/src/executors/codex.js +436 -0
- package/src/executors/commandcode.js +83 -0
- package/src/executors/cursor.js +1075 -0
- package/src/executors/default.js +294 -0
- package/src/executors/gemini-cli.js +88 -0
- package/src/executors/github.js +393 -0
- package/src/executors/grok-cli.js +510 -0
- package/src/executors/grok-web.js +342 -0
- package/src/executors/index.js +86 -0
- package/src/executors/kimchi.js +116 -0
- package/src/executors/kiro.js +1155 -0
- package/src/executors/ollama-local.js +14 -0
- package/src/executors/opencode-go.js +47 -0
- package/src/executors/opencode.js +31 -0
- package/src/executors/qoder.js +433 -0
- package/src/executors/trae.js +318 -0
- package/src/executors/vertex.js +151 -0
- package/src/executors/windsurf.js +555 -0
- package/src/executors/xiaomi-tokenplan.js +16 -0
- package/src/handlers/chatCore/nonStreamingHandler.js +370 -0
- package/src/handlers/chatCore/requestDetail.js +130 -0
- package/src/handlers/chatCore/sseToJsonHandler.js +318 -0
- package/src/handlers/chatCore/streamingHandler.js +250 -0
- package/src/handlers/chatCore.js +652 -0
- package/src/lib/auth/dashboardSession.js +83 -0
- package/src/lib/auth/loginLimiter.js +99 -0
- package/src/lib/cache/ttlCache.js +153 -0
- package/src/lib/consoleLogBuffer.js +127 -0
- package/src/lib/dataDir.js +45 -0
- package/src/lib/db/adapters/betterSqliteAdapter.js +36 -0
- package/src/lib/db/adapters/bunSqliteAdapter.js +47 -0
- package/src/lib/db/adapters/nodeSqliteAdapter.js +65 -0
- package/src/lib/db/adapters/runtimeHelpers.js +23 -0
- package/src/lib/db/adapters/sqljsAdapter.js +103 -0
- package/src/lib/db/backup.js +75 -0
- package/src/lib/db/driver.js +110 -0
- package/src/lib/db/helpers/jsonCol.js +18 -0
- package/src/lib/db/helpers/kvStore.js +48 -0
- package/src/lib/db/helpers/metaStore.js +32 -0
- package/src/lib/db/index.js +473 -0
- package/src/lib/db/migrate.js +267 -0
- package/src/lib/db/migrations/001-initial.js +12 -0
- package/src/lib/db/migrations/002-prune-headroom-pxpipe-settings.js +32 -0
- package/src/lib/db/migrations/004-request-details-trace-id.js +12 -0
- package/src/lib/db/migrations/005-request-details-payload-capture.js +11 -0
- package/src/lib/db/migrations/007-headroom-readd-settings.js +35 -0
- package/src/lib/db/migrations/008-account-model-locks.js +22 -0
- package/src/lib/db/migrations/009-warp-pool-lifecycle.js +5 -0
- package/src/lib/db/migrations/010-api-key-file-permissions.js +7 -0
- package/src/lib/db/migrations/011-remove-upstream-user-agent-profile.js +41 -0
- package/src/lib/db/migrations/012-remove-headroom-settings.js +33 -0
- package/src/lib/db/migrations/013-default-fill-first-routing.js +20 -0
- package/src/lib/db/migrations/014-normalize-fill-first-routing.js +37 -0
- package/src/lib/db/migrations/015-remove-ip-ban-tables.js +8 -0
- package/src/lib/db/migrations/016-prune-legacy-codebuddy-intl-models.js +25 -0
- package/src/lib/db/migrations/index.js +20 -0
- package/src/lib/db/nineRouterMigration.js +785 -0
- package/src/lib/db/paths.js +23 -0
- package/src/lib/db/repos/accountModelLocksRepo.js +74 -0
- package/src/lib/db/repos/aliasRepo.js +108 -0
- package/src/lib/db/repos/apiKeysRepo.js +204 -0
- package/src/lib/db/repos/combosRepo.js +85 -0
- package/src/lib/db/repos/connectionsRepo.js +522 -0
- package/src/lib/db/repos/disabledModelsRepo.js +63 -0
- package/src/lib/db/repos/nodesRepo.js +127 -0
- package/src/lib/db/repos/pricingRepo.js +108 -0
- package/src/lib/db/repos/proxyPoolsRepo.js +104 -0
- package/src/lib/db/repos/requestDetailsRepo.js +345 -0
- package/src/lib/db/repos/settingsRepo.js +135 -0
- package/src/lib/db/repos/usageRepo.js +1335 -0
- package/src/lib/db/schema.js +179 -0
- package/src/lib/db/version.js +32 -0
- package/src/lib/db/writeBehind.js +174 -0
- package/src/lib/disabledModelsDb.js +3 -0
- package/src/lib/env.ts +175 -0
- package/src/lib/errors/publicError.js +18 -0
- package/src/lib/grokBuildConfig.js +243 -0
- package/src/lib/guardRequest.js +51 -0
- package/src/lib/localDb.js +21 -0
- package/src/lib/mcp/stdioSseBridge.js +174 -0
- package/src/lib/network/connectionProxy.js +200 -0
- package/src/lib/network/proxyTest.js +158 -0
- package/src/lib/network/proxyUrl.js +76 -0
- package/src/lib/network/relay.js +71 -0
- package/src/lib/oauth/constants/oauth.js +152 -0
- package/src/lib/oauth/constants/xai.js +43 -0
- package/src/lib/oauth/kiroExternalIdp.js +167 -0
- package/src/lib/oauth/providerHelpers.js +90 -0
- package/src/lib/oauth/providers/_shared.js +12 -0
- package/src/lib/oauth/providers/antigravity.js +126 -0
- package/src/lib/oauth/providers/claude.js +60 -0
- package/src/lib/oauth/providers/cline.js +62 -0
- package/src/lib/oauth/providers/clinepass.js +62 -0
- package/src/lib/oauth/providers/codebuddy-cn.js +75 -0
- package/src/lib/oauth/providers/codebuddy-intl.js +73 -0
- package/src/lib/oauth/providers/codex.js +72 -0
- package/src/lib/oauth/providers/cursor.js +18 -0
- package/src/lib/oauth/providers/gemini-cli.js +84 -0
- package/src/lib/oauth/providers/github.js +96 -0
- package/src/lib/oauth/providers/grok-cli.js +123 -0
- package/src/lib/oauth/providers/index.js +233 -0
- package/src/lib/oauth/providers/kilocode.js +60 -0
- package/src/lib/oauth/providers/kimchi.js +79 -0
- package/src/lib/oauth/providers/kimi.js +79 -0
- package/src/lib/oauth/providers/kiro.js +147 -0
- package/src/lib/oauth/providers/qoder.js +90 -0
- package/src/lib/oauth/providers/trae.js +244 -0
- package/src/lib/oauth/providers/windsurf.js +123 -0
- package/src/lib/oauth/providers/xai.js +95 -0
- package/src/lib/oauth/providers.js +16 -0
- package/src/lib/oauth/services/cursor.js +132 -0
- package/src/lib/oauth/services/kiro.js +351 -0
- package/src/lib/oauth/services/oauth.js +135 -0
- package/src/lib/oauth/services/qoder.js +151 -0
- package/src/lib/oauth/services/xai.js +204 -0
- package/src/lib/oauth/utils/ideDetect.js +55 -0
- package/src/lib/oauth/utils/pkce.js +25 -0
- package/src/lib/oauth/utils/server.js +732 -0
- package/src/lib/oauth/utils/ui.js +43 -0
- package/src/lib/providerNormalization.js +40 -0
- package/src/lib/qoder/constants.js +1 -0
- package/src/lib/requestDetailsDb.js +3 -0
- package/src/lib/resourceProfile.ts +124 -0
- package/src/lib/runtimeSecrets.ts +130 -0
- package/src/lib/security/payloadCapture.js +87 -0
- package/src/lib/tunnel/cloudflare/cloudflared.js +436 -0
- package/src/lib/tunnel/cloudflare/config.js +8 -0
- package/src/lib/tunnel/cloudflare/healthCheck.js +37 -0
- package/src/lib/tunnel/cloudflare/manager.js +149 -0
- package/src/lib/tunnel/cloudflare/pid.js +31 -0
- package/src/lib/tunnel/cloudflare/publicUrl.js +41 -0
- package/src/lib/tunnel/cloudflare/requirements.js +12 -0
- package/src/lib/tunnel/index.js +26 -0
- package/src/lib/tunnel/shared/dnsResolver.js +16 -0
- package/src/lib/tunnel/shared/internetCheck.js +30 -0
- package/src/lib/tunnel/shared/state.js +45 -0
- package/src/lib/tunnel/shared/watchdogConfig.js +6 -0
- package/src/lib/update/installer.js +52 -0
- package/src/lib/update/version.js +116 -0
- package/src/lib/usageDb.js +6 -0
- package/src/lib/usageRefresh.js +88 -0
- package/src/lifecycle/appLifecycle.ts +99 -0
- package/src/models.js +36 -0
- package/src/next/headers.js +12 -0
- package/src/next/server.js +74 -0
- package/src/observability/metrics.js +439 -0
- package/src/observability/tracing.js +107 -0
- package/src/open-sse-index.js +70 -0
- package/src/providers/capabilities.js +299 -0
- package/src/providers/display.ts +142 -0
- package/src/providers/index.js +27 -0
- package/src/providers/models/helpers.js +19 -0
- package/src/providers/models/namePatterns.js +27 -0
- package/src/providers/models/schema.js +32 -0
- package/src/providers/pricing.js +248 -0
- package/src/providers/registry/agentrouter.js +52 -0
- package/src/providers/registry/alicode-intl.js +32 -0
- package/src/providers/registry/alicode.js +33 -0
- package/src/providers/registry/alims-intl.js +30 -0
- package/src/providers/registry/anthropic.js +29 -0
- package/src/providers/registry/antigravity.js +139 -0
- package/src/providers/registry/azure.js +23 -0
- package/src/providers/registry/baidu.js +35 -0
- package/src/providers/registry/blackbox.js +41 -0
- package/src/providers/registry/byteplus.js +36 -0
- package/src/providers/registry/cerebras.js +33 -0
- package/src/providers/registry/chutes.js +26 -0
- package/src/providers/registry/claude.js +80 -0
- package/src/providers/registry/cline.js +53 -0
- package/src/providers/registry/clinepass.js +62 -0
- package/src/providers/registry/cloudflare-ai.js +84 -0
- package/src/providers/registry/codebuddy-cn.js +95 -0
- package/src/providers/registry/codebuddy-intl.js +91 -0
- package/src/providers/registry/codex.js +145 -0
- package/src/providers/registry/commandcode.js +45 -0
- package/src/providers/registry/cursor.js +61 -0
- package/src/providers/registry/deepseek.js +55 -0
- package/src/providers/registry/featherless.js +36 -0
- package/src/providers/registry/fireworks.js +34 -0
- package/src/providers/registry/gemini-cli.js +59 -0
- package/src/providers/registry/gemini.js +76 -0
- package/src/providers/registry/github.js +131 -0
- package/src/providers/registry/glm-cn.js +35 -0
- package/src/providers/registry/glm.js +58 -0
- package/src/providers/registry/grok-cli.js +87 -0
- package/src/providers/registry/grok-web.js +41 -0
- package/src/providers/registry/groq.js +38 -0
- package/src/providers/registry/index.js +77 -0
- package/src/providers/registry/kilo-gateway.js +34 -0
- package/src/providers/registry/kilocode.js +48 -0
- package/src/providers/registry/kimchi.js +49 -0
- package/src/providers/registry/kimi.js +82 -0
- package/src/providers/registry/kiro.js +132 -0
- package/src/providers/registry/meta.js +34 -0
- package/src/providers/registry/minimax-cn.js +90 -0
- package/src/providers/registry/minimax.js +90 -0
- package/src/providers/registry/mistral.js +36 -0
- package/src/providers/registry/morph.js +31 -0
- package/src/providers/registry/nebius.js +26 -0
- package/src/providers/registry/nvidia.js +55 -0
- package/src/providers/registry/ollama-local.js +18 -0
- package/src/providers/registry/ollama.js +38 -0
- package/src/providers/registry/openai.js +114 -0
- package/src/providers/registry/opencode-go.js +41 -0
- package/src/providers/registry/opencode.js +26 -0
- package/src/providers/registry/openrouter.js +36 -0
- package/src/providers/registry/perplexity.js +56 -0
- package/src/providers/registry/poolside.js +30 -0
- package/src/providers/registry/qoder.js +58 -0
- package/src/providers/registry/sumopod.js +84 -0
- package/src/providers/registry/tencent.js +29 -0
- package/src/providers/registry/together.js +38 -0
- package/src/providers/registry/trae.js +71 -0
- package/src/providers/registry/venice.js +69 -0
- package/src/providers/registry/vercel-ai-gateway.js +37 -0
- package/src/providers/registry/vertex-partner.js +31 -0
- package/src/providers/registry/vertex.js +33 -0
- package/src/providers/registry/windsurf.js +129 -0
- package/src/providers/registry/xai.js +46 -0
- package/src/providers/registry/xiaomi-mimo.js +66 -0
- package/src/providers/registry/xiaomi-tokenplan.js +58 -0
- package/src/providers/registryEntry.js +3 -0
- package/src/providers/schema.js +33 -0
- package/src/providers/shared.js +114 -0
- package/src/providers/thinkingLevels.js +49 -0
- package/src/routes/api/auth/login/route.js +82 -0
- package/src/routes/api/auth/logout/route.js +9 -0
- package/src/routes/api/auth/reset-password/route.js +12 -0
- package/src/routes/api/auth/status/route.js +33 -0
- package/src/routes/api/auth/verify-password/route.js +39 -0
- package/src/routes/api/chat/tools/curl/route.js +100 -0
- package/src/routes/api/chat/tools/router/route.js +215 -0
- package/src/routes/api/chat/tools/search/route.js +362 -0
- package/src/routes/api/chat/tools/shell/route.js +228 -0
- package/src/routes/api/cli-tools/all-statuses/route.js +37 -0
- package/src/routes/api/cli-tools/claude-settings/route.js +261 -0
- package/src/routes/api/cli-tools/codex-settings/route.js +220 -0
- package/src/routes/api/cli-tools/cowork-mcp-registry/route.js +80 -0
- package/src/routes/api/cli-tools/cowork-mcp-tools/route.js +96 -0
- package/src/routes/api/cli-tools/cowork-settings/route.js +380 -0
- package/src/routes/api/cli-tools/grok-build-settings/route.js +152 -0
- package/src/routes/api/cli-tools/hermes-settings/route.js +169 -0
- package/src/routes/api/cli-tools/kilo-settings/route.js +131 -0
- package/src/routes/api/cli-tools/model-mappings/route.js +43 -0
- package/src/routes/api/cli-tools/omp-settings/route.js +266 -0
- package/src/routes/api/cli-tools/opencode-settings/route.js +243 -0
- package/src/routes/api/combos/[id]/route.js +73 -0
- package/src/routes/api/combos/route.js +43 -0
- package/src/routes/api/health/live/route.js +7 -0
- package/src/routes/api/health/ready/route.js +19 -0
- package/src/routes/api/health/route.js +17 -0
- package/src/routes/api/inflight/stream/route.js +24 -0
- package/src/routes/api/init/route.js +3 -0
- package/src/routes/api/keys/[id]/route.js +71 -0
- package/src/routes/api/keys/route.js +41 -0
- package/src/routes/api/logs/route.js +19 -0
- package/src/routes/api/mcp/[plugin]/message/route.js +23 -0
- package/src/routes/api/mcp/[plugin]/sse/route.js +35 -0
- package/src/routes/api/models/availability/route.js +104 -0
- package/src/routes/api/models/custom/route.js +58 -0
- package/src/routes/api/models/disabled/route.js +47 -0
- package/src/routes/api/models/route.js +64 -0
- package/src/routes/api/models/test/ping.js +236 -0
- package/src/routes/api/models/test/route.js +14 -0
- package/src/routes/api/oauth/[provider]/[action]/route.js +491 -0
- package/src/routes/api/oauth/codex/bulk-import/route.js +105 -0
- package/src/routes/api/oauth/codex/import-token/route.js +82 -0
- package/src/routes/api/oauth/cursor/auto-import/route.js +250 -0
- package/src/routes/api/oauth/cursor/import/route.js +86 -0
- package/src/routes/api/oauth/kiro/api-key/route.js +60 -0
- package/src/routes/api/oauth/kiro/auto-import/route.js +120 -0
- package/src/routes/api/oauth/kiro/import/route.js +72 -0
- package/src/routes/api/oauth/kiro/import-cli-proxy/route.js +42 -0
- package/src/routes/api/oauth/kiro/social-authorize/route.js +40 -0
- package/src/routes/api/oauth/kiro/social-exchange/route.js +62 -0
- package/src/routes/api/pricing/route.js +96 -0
- package/src/routes/api/provider-nodes/[id]/route.js +106 -0
- package/src/routes/api/provider-nodes/route.js +110 -0
- package/src/routes/api/provider-nodes/validate/route.js +197 -0
- package/src/routes/api/providers/[id]/models/route.js +716 -0
- package/src/routes/api/providers/[id]/route.js +195 -0
- package/src/routes/api/providers/[id]/test/route.js +27 -0
- package/src/routes/api/providers/[id]/test/testUtils.js +912 -0
- package/src/routes/api/providers/[id]/test-models/route.js +211 -0
- package/src/routes/api/providers/catalog/route.js +58 -0
- package/src/routes/api/providers/client/route.js +133 -0
- package/src/routes/api/providers/counts/route.js +43 -0
- package/src/routes/api/providers/kilo/free-models/route.js +58 -0
- package/src/routes/api/providers/route.js +361 -0
- package/src/routes/api/providers/suggested-models/filters.js +24 -0
- package/src/routes/api/providers/suggested-models/route.js +34 -0
- package/src/routes/api/providers/test-batch/route.js +145 -0
- package/src/routes/api/providers/validate/route.js +633 -0
- package/src/routes/api/proxy-pools/[id]/route.js +129 -0
- package/src/routes/api/proxy-pools/[id]/test/route.js +64 -0
- package/src/routes/api/proxy-pools/cloudflare-deploy/route.js +183 -0
- package/src/routes/api/proxy-pools/route.js +85 -0
- package/src/routes/api/proxy-pools/vercel-deploy/route.js +181 -0
- package/src/routes/api/settings/currency/route.js +29 -0
- package/src/routes/api/settings/database/path/route.js +8 -0
- package/src/routes/api/settings/database/route.js +89 -0
- package/src/routes/api/settings/migrate-9router/route.js +81 -0
- package/src/routes/api/settings/require-login/route.js +14 -0
- package/src/routes/api/settings/route.js +186 -0
- package/src/routes/api/shutdown/route.js +25 -0
- package/src/routes/api/tags/route.js +17 -0
- package/src/routes/api/translator/console-logs/route.js +28 -0
- package/src/routes/api/translator/console-logs/stream/route.js +95 -0
- package/src/routes/api/tunnel/disable/route.js +18 -0
- package/src/routes/api/tunnel/enable/route.js +32 -0
- package/src/routes/api/tunnel/status/route.js +23 -0
- package/src/routes/api/update/route.js +42 -0
- package/src/routes/api/usage/[connectionId]/codex-reset-credits/route.js +169 -0
- package/src/routes/api/usage/[connectionId]/route.js +222 -0
- package/src/routes/api/usage/chart/route.js +27 -0
- package/src/routes/api/usage/history/route.js +94 -0
- package/src/routes/api/usage/logs/route.js +14 -0
- package/src/routes/api/usage/providers/route.js +36 -0
- package/src/routes/api/usage/request-details/route.js +61 -0
- package/src/routes/api/usage/request-logs/route.js +15 -0
- package/src/routes/api/usage/stats/route.js +23 -0
- package/src/routes/api/usage/stream/route.js +78 -0
- package/src/routes/api/v1/api/chat/route.js +36 -0
- package/src/routes/api/v1/chat/completions/route.js +28 -0
- package/src/routes/api/v1/images/edits/route.js +70 -0
- package/src/routes/api/v1/images/generations/route.js +50 -0
- package/src/routes/api/v1/images/imageValidation.js +45 -0
- package/src/routes/api/v1/messages/count_tokens/route.js +87 -0
- package/src/routes/api/v1/messages/route.js +26 -0
- package/src/routes/api/v1/models/route.js +440 -0
- package/src/routes/api/v1/responses/compact/route.js +34 -0
- package/src/routes/api/v1/responses/route.js +26 -0
- package/src/routes/api/v1/route.js +1 -0
- package/src/routes/api/version/route.js +11 -0
- package/src/routes/table.js +1141 -0
- package/src/rtk/applyFilter.js +13 -0
- package/src/rtk/autodetect.js +104 -0
- package/src/rtk/caveman.js +6 -0
- package/src/rtk/cavemanPrompts.js +107 -0
- package/src/rtk/constants.js +51 -0
- package/src/rtk/filters/buildOutput.js +120 -0
- package/src/rtk/filters/dedupLog.js +43 -0
- package/src/rtk/filters/find.js +47 -0
- package/src/rtk/filters/gitDiff.js +90 -0
- package/src/rtk/filters/gitLog.js +88 -0
- package/src/rtk/filters/gitStatus.js +101 -0
- package/src/rtk/filters/grep.js +45 -0
- package/src/rtk/filters/ls.js +73 -0
- package/src/rtk/filters/readNumbered.js +22 -0
- package/src/rtk/filters/searchList.js +49 -0
- package/src/rtk/filters/smartTruncate.js +13 -0
- package/src/rtk/filters/tree.js +28 -0
- package/src/rtk/index.js +143 -0
- package/src/rtk/ponytail.js +6 -0
- package/src/rtk/ponytailPrompt.js +49 -0
- package/src/rtk/systemInject.js +89 -0
- package/src/server.ts +474 -0
- package/src/services/accountFallback.js +241 -0
- package/src/services/accountPool.js +389 -0
- package/src/services/capacityAdapter.js +140 -0
- package/src/services/cliModelMapping.js +84 -0
- package/src/services/clinepassModels.js +60 -0
- package/src/services/codebuddyModels.js +250 -0
- package/src/services/combo.js +482 -0
- package/src/services/copilotModels.js +126 -0
- package/src/services/currencyRate.js +56 -0
- package/src/services/cursorModels.js +197 -0
- package/src/services/grokCliModels.js +130 -0
- package/src/services/kimchiModels.js +175 -0
- package/src/services/kiroModels.js +246 -0
- package/src/services/model.js +83 -0
- package/src/services/oauthCredentialManager.js +153 -0
- package/src/services/projectId.js +240 -0
- package/src/services/provider.js +145 -0
- package/src/services/qoderModels.js +274 -0
- package/src/services/tokenRefresh/dedup.js +70 -0
- package/src/services/tokenRefresh/providers.js +626 -0
- package/src/services/tokenRefresh.js +274 -0
- package/src/services/usage/claude.js +134 -0
- package/src/services/usage/codebuddy-cn.js +212 -0
- package/src/services/usage/codex.js +193 -0
- package/src/services/usage/deepseek.js +103 -0
- package/src/services/usage/github.js +94 -0
- package/src/services/usage/google.js +218 -0
- package/src/services/usage/grok-cli.js +368 -0
- package/src/services/usage/grokCliQuotaFrame.js +170 -0
- package/src/services/usage/kimi.js +185 -0
- package/src/services/usage/kiro.js +172 -0
- package/src/services/usage/minimax.js +215 -0
- package/src/services/usage/misc.js +264 -0
- package/src/services/usage/shared.js +59 -0
- package/src/services/usage.js +56 -0
- package/src/shared/clineAuth.js +37 -0
- package/src/shared/constants/config.js +80 -0
- package/src/shared/constants/coworkPlugins.js +69 -0
- package/src/shared/constants/models.js +41 -0
- package/src/shared/constants/providers.js +128 -0
- package/src/shared/constants/providersDisplay.js +1 -0
- package/src/shared/qoder/constants.js +50 -0
- package/src/shared/qoder/cosy.js +134 -0
- package/src/shared/qoder/encoding.js +36 -0
- package/src/shared/services/initializeApp.js +226 -0
- package/src/shared/services/quotaAutoPing.js +310 -0
- package/src/shared/utils/apiKey.js +65 -0
- package/src/shared/utils/clineAuth.js +1 -0
- package/src/shared/utils/index.js +3 -0
- package/src/shared/utils/machineId.js +60 -0
- package/src/shared/utils/ssrfGuard.js +280 -0
- package/src/sse/handlers/chat.js +426 -0
- package/src/sse/oauthLease.js +32 -0
- package/src/sse/recovery.js +235 -0
- package/src/sse/services/auth.js +489 -0
- package/src/sse/services/backgroundTokenRefresh.js +173 -0
- package/src/sse/services/model.js +129 -0
- package/src/sse/services/requestCancellation.js +4 -0
- package/src/sse/services/tokenRefresh.js +264 -0
- package/src/sse/utils/logger.js +142 -0
- package/src/staticServe.ts +95 -0
- package/src/transformer/streamToJsonConverter.js +87 -0
- package/src/translator/canonical.js +81 -0
- package/src/translator/concerns/chunk.js +9 -0
- package/src/translator/concerns/compatFallback.js +147 -0
- package/src/translator/concerns/finishReason.js +59 -0
- package/src/translator/concerns/image.js +105 -0
- package/src/translator/concerns/json.js +4 -0
- package/src/translator/concerns/kiroConversation.js +428 -0
- package/src/translator/concerns/message.js +5 -0
- package/src/translator/concerns/modality.js +134 -0
- package/src/translator/concerns/paramSupport.js +97 -0
- package/src/translator/concerns/prefetch.js +86 -0
- package/src/translator/concerns/reasoning.js +18 -0
- package/src/translator/concerns/thinking.js +40 -0
- package/src/translator/concerns/thinkingUnified.js +319 -0
- package/src/translator/concerns/toolCall.js +134 -0
- package/src/translator/concerns/usage.js +61 -0
- package/src/translator/detection.js +42 -0
- package/src/translator/formats/claude.js +323 -0
- package/src/translator/formats/gemini.js +366 -0
- package/src/translator/formats/maxTokens.js +19 -0
- package/src/translator/formats/openai.js +116 -0
- package/src/translator/formats/responsesApi.js +117 -0
- package/src/translator/formats.js +28 -0
- package/src/translator/index.js +237 -0
- package/src/translator/request/antigravity-to-openai.js +217 -0
- package/src/translator/request/claude-to-kiro.js +288 -0
- package/src/translator/request/claude-to-openai.js +199 -0
- package/src/translator/request/gemini-to-openai.js +142 -0
- package/src/translator/request/openai-responses.js +373 -0
- package/src/translator/request/openai-to-claude.js +260 -0
- package/src/translator/request/openai-to-commandcode.js +161 -0
- package/src/translator/request/openai-to-cursor.js +175 -0
- package/src/translator/request/openai-to-gemini.js +422 -0
- package/src/translator/request/openai-to-kiro.js +360 -0
- package/src/translator/request/openai-to-ollama.js +152 -0
- package/src/translator/request/openai-to-vertex.js +36 -0
- package/src/translator/response/claude-to-openai.js +185 -0
- package/src/translator/response/commandcode-to-openai.js +164 -0
- package/src/translator/response/cursor-to-openai.js +18 -0
- package/src/translator/response/gemini-to-openai.js +127 -0
- package/src/translator/response/kiro-to-claude.js +230 -0
- package/src/translator/response/kiro-to-openai.js +136 -0
- package/src/translator/response/ollama-to-openai.js +106 -0
- package/src/translator/response/openai-responses.js +534 -0
- package/src/translator/response/openai-to-antigravity.js +107 -0
- package/src/translator/response/openai-to-claude.js +231 -0
- package/src/translator/schema/blocks.js +40 -0
- package/src/translator/schema/defaults.js +3 -0
- package/src/translator/schema/finishReasons.js +22 -0
- package/src/translator/schema/index.js +7 -0
- package/src/translator/schema/roles.js +12 -0
- package/src/transport/requestBoundary.ts +223 -0
- package/src/transport/routerAdmission.ts +380 -0
- package/src/types/provider.ts +59 -0
- package/src/utils/bypassHandler.js +272 -0
- package/src/utils/claudeCloaking.js +10 -0
- package/src/utils/claudeSignature.js +35 -0
- package/src/utils/clientDetector.js +183 -0
- package/src/utils/cursorChecksum.js +104 -0
- package/src/utils/cursorProtobuf.js +801 -0
- package/src/utils/debugLog.js +12 -0
- package/src/utils/error.js +120 -0
- package/src/utils/inflight.js +127 -0
- package/src/utils/kiroSessionReplay.js +139 -0
- package/src/utils/ollamaTransform.js +83 -0
- package/src/utils/proxyFetch.js +308 -0
- package/src/utils/reasoningContentInjector.js +71 -0
- package/src/utils/requestLogger.js +272 -0
- package/src/utils/responsesStreamHelpers.js +46 -0
- package/src/utils/sessionManager.js +205 -0
- package/src/utils/sse.js +13 -0
- package/src/utils/sseConstants.js +21 -0
- package/src/utils/stream.js +511 -0
- package/src/utils/streamHandler.js +256 -0
- package/src/utils/streamHelpers.js +113 -0
- package/src/utils/toolDeduper.js +44 -0
- package/src/utils/usageTracking.js +329 -0
- package/tsconfig.json +45 -0
|
@@ -0,0 +1,1335 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
import { getAdapter } from "../driver.js";
|
|
3
|
+
import { parseJson, stringifyJson } from "../helpers/jsonCol.js";
|
|
4
|
+
import { getMeta, setMeta } from "../helpers/metaStore.js";
|
|
5
|
+
import { getWriteBehindBuffer } from "../writeBehind.js";
|
|
6
|
+
import { env } from "@/lib/env";
|
|
7
|
+
|
|
8
|
+
function maskApiKey(key) {
|
|
9
|
+
if (!key || typeof key !== "string") return null;
|
|
10
|
+
if (key.length <= 8) return key.charAt(0) + "***";
|
|
11
|
+
return key.slice(0, 8) + "***";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const PENDING_TIMEOUT_MS = 60 * 1000;
|
|
15
|
+
const RING_CAP = 50;
|
|
16
|
+
const CONN_CACHE_TTL_MS = 30 * 1000;
|
|
17
|
+
const PERIOD_MS = { "24h": 86400000, "7d": 604800000, "30d": 2592000000, "60d": 5184000000, "90d": 7776000000, "365d": 31536000000 };
|
|
18
|
+
const HISTORY_PRUNE_INTERVAL_MS = 60 * 1000;
|
|
19
|
+
const HISTORY_PRUNE_BATCH_SIZE = 10_000;
|
|
20
|
+
let lastHistoryPruneAt = 0;
|
|
21
|
+
|
|
22
|
+
if (!global._pendingRequests) global._pendingRequests = { byModel: {}, byAccount: {} };
|
|
23
|
+
if (!global._lastErrorProvider) global._lastErrorProvider = { provider: "", ts: 0 };
|
|
24
|
+
if (!global._statsEmitter) {
|
|
25
|
+
global._statsEmitter = new EventEmitter();
|
|
26
|
+
global._statsEmitter.setMaxListeners(50);
|
|
27
|
+
}
|
|
28
|
+
if (!global._pendingTimers) global._pendingTimers = {};
|
|
29
|
+
if (!global._recentRing) global._recentRing = { items: [], initialized: false };
|
|
30
|
+
if (!global._connectionMapCache) global._connectionMapCache = { map: {}, ts: 0 };
|
|
31
|
+
if (!global._statsEmitTimers) global._statsEmitTimers = { pending: null, update: null };
|
|
32
|
+
|
|
33
|
+
const pendingRequests = global._pendingRequests;
|
|
34
|
+
const lastErrorProvider = global._lastErrorProvider;
|
|
35
|
+
const pendingTimers = global._pendingTimers;
|
|
36
|
+
const recentRing = global._recentRing;
|
|
37
|
+
const connCache = global._connectionMapCache;
|
|
38
|
+
const statsEmitTimers = global._statsEmitTimers;
|
|
39
|
+
|
|
40
|
+
export const statsEmitter = global._statsEmitter;
|
|
41
|
+
|
|
42
|
+
function scheduleStatsEvent(event, delayMs = 150) {
|
|
43
|
+
const key = event === "update" ? "update" : "pending";
|
|
44
|
+
if (statsEmitTimers[key]) return;
|
|
45
|
+
statsEmitTimers[key] = setTimeout(() => {
|
|
46
|
+
statsEmitTimers[key] = null;
|
|
47
|
+
statsEmitter.emit(event);
|
|
48
|
+
}, delayMs);
|
|
49
|
+
statsEmitTimers[key]?.unref?.();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getLocalDateKey(timestamp) {
|
|
53
|
+
const d = timestamp ? new Date(timestamp) : new Date();
|
|
54
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isUsageErrorStatus(status) {
|
|
58
|
+
const value = String(status || "").toLowerCase();
|
|
59
|
+
return value === "error" || value.includes("error") || value.startsWith("4") || value.startsWith("5");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function addToCounter(target, key, values) {
|
|
63
|
+
if (!target[key]) target[key] = { requests: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cost: 0 };
|
|
64
|
+
target[key].requests += values.requests || 1;
|
|
65
|
+
target[key].promptTokens += values.promptTokens || 0;
|
|
66
|
+
target[key].completionTokens += values.completionTokens || 0;
|
|
67
|
+
target[key].cachedTokens += values.cachedTokens || 0;
|
|
68
|
+
target[key].cost += values.cost || 0;
|
|
69
|
+
if (values.meta) Object.assign(target[key], values.meta);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function aggregateEntryToDay(day, entry) {
|
|
73
|
+
const promptTokens = entry.tokens?.prompt_tokens || entry.tokens?.input_tokens || 0;
|
|
74
|
+
const completionTokens = entry.tokens?.completion_tokens || entry.tokens?.output_tokens || 0;
|
|
75
|
+
const cachedTokens = entry.tokens?.cached_tokens || entry.tokens?.cache_read_input_tokens || 0;
|
|
76
|
+
const cost = entry.cost || 0;
|
|
77
|
+
const vals = { promptTokens, completionTokens, cachedTokens, cost };
|
|
78
|
+
|
|
79
|
+
day.requests = (day.requests || 0) + 1;
|
|
80
|
+
day.promptTokens = (day.promptTokens || 0) + promptTokens;
|
|
81
|
+
day.completionTokens = (day.completionTokens || 0) + completionTokens;
|
|
82
|
+
day.cachedTokens = (day.cachedTokens || 0) + cachedTokens;
|
|
83
|
+
day.cost = (day.cost || 0) + cost;
|
|
84
|
+
|
|
85
|
+
day.byProvider ||= {};
|
|
86
|
+
day.byModel ||= {};
|
|
87
|
+
day.byAccount ||= {};
|
|
88
|
+
day.byApiKey ||= {};
|
|
89
|
+
day.byEndpoint ||= {};
|
|
90
|
+
|
|
91
|
+
if (entry.provider) addToCounter(day.byProvider, entry.provider, vals);
|
|
92
|
+
|
|
93
|
+
const modelKey = entry.provider ? `${entry.model}|${entry.provider}` : entry.model;
|
|
94
|
+
addToCounter(day.byModel, modelKey, { ...vals, meta: { rawModel: entry.model, provider: entry.provider } });
|
|
95
|
+
|
|
96
|
+
if (entry.connectionId) {
|
|
97
|
+
addToCounter(day.byAccount, entry.connectionId, { ...vals, meta: { rawModel: entry.model, provider: entry.provider } });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const apiKeyVal = entry.apiKey && typeof entry.apiKey === "string" ? entry.apiKey : "local-no-key";
|
|
101
|
+
const akModelKey = `${apiKeyVal}|${entry.model}|${entry.provider || "unknown"}`;
|
|
102
|
+
addToCounter(day.byApiKey, akModelKey, { ...vals, meta: { rawModel: entry.model, provider: entry.provider, apiKey: entry.apiKey || null } });
|
|
103
|
+
|
|
104
|
+
const endpoint = entry.endpoint || "Unknown";
|
|
105
|
+
const epKey = `${endpoint}|${entry.model}|${entry.provider || "unknown"}`;
|
|
106
|
+
addToCounter(day.byEndpoint, epKey, { ...vals, meta: { endpoint, rawModel: entry.model, provider: entry.provider } });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function createEmptyUsageDay() {
|
|
110
|
+
return {
|
|
111
|
+
requests: 0,
|
|
112
|
+
promptTokens: 0,
|
|
113
|
+
completionTokens: 0,
|
|
114
|
+
cachedTokens: 0,
|
|
115
|
+
cost: 0,
|
|
116
|
+
byProvider: {},
|
|
117
|
+
byModel: {},
|
|
118
|
+
byAccount: {},
|
|
119
|
+
byApiKey: {},
|
|
120
|
+
byEndpoint: {},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function pruneUsageHistory(db) {
|
|
125
|
+
const now = Date.now();
|
|
126
|
+
if (now - lastHistoryPruneAt < HISTORY_PRUNE_INTERVAL_MS) return 0;
|
|
127
|
+
|
|
128
|
+
const retentionDays = Math.max(1, Number(env.usageHistoryRetentionDays) || 7);
|
|
129
|
+
const maxRows = Math.max(1, Number(env.usageHistoryMaxRows) || 500_000);
|
|
130
|
+
const cutoff = new Date(now - retentionDays * 86400000).toISOString();
|
|
131
|
+
let deleted = 0;
|
|
132
|
+
|
|
133
|
+
const oldRows = db.all(
|
|
134
|
+
"SELECT id FROM usageHistory WHERE timestamp < ? ORDER BY id ASC LIMIT ?",
|
|
135
|
+
[cutoff, HISTORY_PRUNE_BATCH_SIZE],
|
|
136
|
+
);
|
|
137
|
+
if (oldRows.length > 0) {
|
|
138
|
+
db.run(
|
|
139
|
+
"DELETE FROM usageHistory WHERE id IN (SELECT id FROM usageHistory WHERE timestamp < ? ORDER BY id ASC LIMIT ?)",
|
|
140
|
+
[cutoff, HISTORY_PRUNE_BATCH_SIZE],
|
|
141
|
+
);
|
|
142
|
+
deleted += oldRows.length;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const count = Number(db.get("SELECT COUNT(*) AS count FROM usageHistory")?.count || 0);
|
|
146
|
+
if (count > maxRows) {
|
|
147
|
+
const overflow = Math.min(count - maxRows, HISTORY_PRUNE_BATCH_SIZE);
|
|
148
|
+
const oldestRows = db.all(
|
|
149
|
+
"SELECT id FROM usageHistory ORDER BY id ASC LIMIT ?",
|
|
150
|
+
[overflow],
|
|
151
|
+
);
|
|
152
|
+
if (oldestRows.length > 0) {
|
|
153
|
+
db.run(
|
|
154
|
+
"DELETE FROM usageHistory WHERE id IN (SELECT id FROM usageHistory ORDER BY id ASC LIMIT ?)",
|
|
155
|
+
[overflow],
|
|
156
|
+
);
|
|
157
|
+
deleted += oldestRows.length;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (deleted > 0) {
|
|
162
|
+
const previous = Number.parseInt(
|
|
163
|
+
db.get("SELECT value FROM _meta WHERE key = 'usageHistoryPrunedTotal'")?.value,
|
|
164
|
+
10,
|
|
165
|
+
) || 0;
|
|
166
|
+
db.run(
|
|
167
|
+
"INSERT INTO _meta(key, value) VALUES('usageHistoryPrunedTotal', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
|
168
|
+
[String(previous + deleted)],
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
lastHistoryPruneAt = now;
|
|
172
|
+
return deleted;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function reconcileUsageAggregates(db) {
|
|
176
|
+
const historyCount = Number(db.get("SELECT COUNT(*) AS count FROM usageHistory")?.count || 0);
|
|
177
|
+
const dailyRows = db.all("SELECT dateKey, data FROM usageDaily");
|
|
178
|
+
const dailyCount = dailyRows.reduce((sum, row) => {
|
|
179
|
+
const day = parseJson(row.data, {}) || {};
|
|
180
|
+
return sum + Number(day.requests || 0);
|
|
181
|
+
}, 0);
|
|
182
|
+
const lifetimeRaw = db.get("SELECT value FROM _meta WHERE key = 'totalRequestsLifetime'")?.value;
|
|
183
|
+
const lifetimeCount = Number.parseInt(lifetimeRaw, 10);
|
|
184
|
+
const prunedRaw = db.get("SELECT value FROM _meta WHERE key = 'usageHistoryPrunedTotal'")?.value;
|
|
185
|
+
const prunedCount = Math.max(0, Number.parseInt(prunedRaw, 10) || 0);
|
|
186
|
+
const expectedRetainedCount = Math.max(0, lifetimeCount - prunedCount);
|
|
187
|
+
|
|
188
|
+
const aggregatesMatch = historyCount === dailyCount
|
|
189
|
+
&& historyCount === lifetimeCount
|
|
190
|
+
&& (historyCount > 0 || dailyRows.length === 0);
|
|
191
|
+
const retainedHistoryMatches = prunedCount > 0
|
|
192
|
+
&& dailyCount === lifetimeCount
|
|
193
|
+
&& historyCount === expectedRetainedCount;
|
|
194
|
+
if (aggregatesMatch || retainedHistoryMatches) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const dayMap = {};
|
|
199
|
+
const rows = db.all(
|
|
200
|
+
"SELECT timestamp, provider, model, connectionId, apiKey, endpoint, promptTokens, completionTokens, cost, status, tokens, meta FROM usageHistory ORDER BY id ASC",
|
|
201
|
+
);
|
|
202
|
+
for (const row of rows) {
|
|
203
|
+
const parsedTokens = parseJson(row.tokens, {}) || {};
|
|
204
|
+
const tokens = Object.keys(parsedTokens).length > 0
|
|
205
|
+
? parsedTokens
|
|
206
|
+
: {
|
|
207
|
+
prompt_tokens: Number(row.promptTokens || 0),
|
|
208
|
+
completion_tokens: Number(row.completionTokens || 0),
|
|
209
|
+
};
|
|
210
|
+
const entry = {
|
|
211
|
+
timestamp: row.timestamp,
|
|
212
|
+
provider: row.provider || null,
|
|
213
|
+
model: row.model || null,
|
|
214
|
+
connectionId: row.connectionId || null,
|
|
215
|
+
apiKey: row.apiKey || null,
|
|
216
|
+
endpoint: row.endpoint || null,
|
|
217
|
+
cost: Number(row.cost || 0),
|
|
218
|
+
status: row.status || "ok",
|
|
219
|
+
tokens,
|
|
220
|
+
meta: parseJson(row.meta, {}) || {},
|
|
221
|
+
};
|
|
222
|
+
const dateKey = getLocalDateKey(row.timestamp);
|
|
223
|
+
dayMap[dateKey] ||= createEmptyUsageDay();
|
|
224
|
+
aggregateEntryToDay(dayMap[dateKey], entry);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
db.transaction(() => {
|
|
228
|
+
db.run("DELETE FROM usageDaily");
|
|
229
|
+
for (const [dateKey, day] of Object.entries(dayMap)) {
|
|
230
|
+
db.run(
|
|
231
|
+
"INSERT INTO usageDaily(dateKey, data) VALUES(?, ?)",
|
|
232
|
+
[dateKey, stringifyJson(day)],
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
db.run(
|
|
236
|
+
"INSERT INTO _meta(key, value) VALUES('totalRequestsLifetime', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
|
237
|
+
[String(historyCount)],
|
|
238
|
+
);
|
|
239
|
+
db.run(
|
|
240
|
+
"INSERT INTO _meta(key, value) VALUES('usageHistoryPrunedTotal', '0') ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function pushToRing(entry) {
|
|
247
|
+
recentRing.items.push(entry);
|
|
248
|
+
if (recentRing.items.length > RING_CAP) {
|
|
249
|
+
recentRing.items = recentRing.items.slice(-RING_CAP);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function finiteLatency(value) {
|
|
254
|
+
const n = Number(value);
|
|
255
|
+
return Number.isFinite(n) && n >= 0 ? n : undefined;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function periodStart(period) {
|
|
259
|
+
if (period === "all") return null;
|
|
260
|
+
if (period === "year") {
|
|
261
|
+
const start = new Date(new Date().getFullYear(), 0, 1);
|
|
262
|
+
return start.toISOString();
|
|
263
|
+
}
|
|
264
|
+
if (period === "today") {
|
|
265
|
+
const start = new Date();
|
|
266
|
+
start.setHours(0, 0, 0, 0);
|
|
267
|
+
return start.toISOString();
|
|
268
|
+
}
|
|
269
|
+
const duration = PERIOD_MS[period];
|
|
270
|
+
return duration ? new Date(Date.now() - duration).toISOString() : null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function enrichRecentLatencies(db, rows) {
|
|
274
|
+
const missing = rows.some((row) => row.latencyMs === undefined);
|
|
275
|
+
if (!missing) return rows;
|
|
276
|
+
|
|
277
|
+
let detailRows = [];
|
|
278
|
+
try {
|
|
279
|
+
detailRows = db.all(
|
|
280
|
+
`SELECT timestamp, provider, model, connectionId, data FROM requestDetails ORDER BY timestamp DESC LIMIT 500`
|
|
281
|
+
);
|
|
282
|
+
} catch {
|
|
283
|
+
return rows;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const candidates = detailRows
|
|
287
|
+
.map((row) => {
|
|
288
|
+
const detail = parseJson(row.data, {}) || {};
|
|
289
|
+
return {
|
|
290
|
+
timestamp: row.timestamp,
|
|
291
|
+
provider: row.provider || "",
|
|
292
|
+
model: row.model || "",
|
|
293
|
+
connectionId: row.connectionId || null,
|
|
294
|
+
time: Date.parse(row.timestamp),
|
|
295
|
+
latencyMs: finiteLatency(detail.latency?.total),
|
|
296
|
+
};
|
|
297
|
+
})
|
|
298
|
+
.filter((row) => Number.isFinite(row.time) && row.latencyMs !== undefined);
|
|
299
|
+
const used = new Set();
|
|
300
|
+
|
|
301
|
+
return rows.map((row) => {
|
|
302
|
+
if (row.latencyMs !== undefined) return row;
|
|
303
|
+
const requestTime = Date.parse(row.timestamp);
|
|
304
|
+
if (!Number.isFinite(requestTime)) return row;
|
|
305
|
+
|
|
306
|
+
let best = null;
|
|
307
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
308
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
309
|
+
if (used.has(i)) continue;
|
|
310
|
+
const candidate = candidates[i];
|
|
311
|
+
if (candidate.provider !== (row.provider || "") || candidate.model !== (row.model || "")) continue;
|
|
312
|
+
if (candidate.connectionId !== (row.connectionId || null)) continue;
|
|
313
|
+
const distance = Math.abs(candidate.time - requestTime);
|
|
314
|
+
if (distance <= 10_000 && distance < bestDistance) {
|
|
315
|
+
best = { index: i, latencyMs: candidate.latencyMs };
|
|
316
|
+
bestDistance = distance;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (!best) return row;
|
|
320
|
+
used.add(best.index);
|
|
321
|
+
return { ...row, latencyMs: best.latencyMs };
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function getConnectionMapCached() {
|
|
326
|
+
if (Date.now() - connCache.ts < CONN_CACHE_TTL_MS) return connCache.map;
|
|
327
|
+
try {
|
|
328
|
+
const { getProviderConnectionsForRouting } = await import("./connectionsRepo.js");
|
|
329
|
+
const all = await getProviderConnectionsForRouting();
|
|
330
|
+
const map = {};
|
|
331
|
+
for (const c of all) map[c.id] = c.name || c.email || c.id;
|
|
332
|
+
connCache.map = map;
|
|
333
|
+
connCache.ts = Date.now();
|
|
334
|
+
} catch {}
|
|
335
|
+
return connCache.map;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function ensureRingInitialized() {
|
|
339
|
+
if (recentRing.initialized) return;
|
|
340
|
+
recentRing.initialized = true;
|
|
341
|
+
try {
|
|
342
|
+
const db = await getAdapter();
|
|
343
|
+
const rows = db.all(`SELECT timestamp, provider, model, connectionId, apiKey, endpoint, cost, status, tokens FROM usageHistory ORDER BY id DESC LIMIT ?`, [RING_CAP]);
|
|
344
|
+
recentRing.items = rows.reverse().map((r) => ({
|
|
345
|
+
timestamp: r.timestamp, provider: r.provider, model: r.model, connectionId: r.connectionId,
|
|
346
|
+
apiKey: r.apiKey, endpoint: r.endpoint, cost: r.cost, status: r.status,
|
|
347
|
+
tokens: parseJson(r.tokens, {}),
|
|
348
|
+
}));
|
|
349
|
+
} catch {}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function calculateCost(provider, model, tokens) {
|
|
353
|
+
if (!tokens || !provider || !model) return 0;
|
|
354
|
+
try {
|
|
355
|
+
const { getPricingForModel } = await import("./pricingRepo.js");
|
|
356
|
+
const pricing = await getPricingForModel(provider, model);
|
|
357
|
+
if (!pricing) return 0;
|
|
358
|
+
|
|
359
|
+
const { calculateCostFromTokens } = await import("../../../providers/pricing.js");
|
|
360
|
+
return calculateCostFromTokens(tokens, pricing);
|
|
361
|
+
} catch (e) {
|
|
362
|
+
console.error("Error calculating cost:", e);
|
|
363
|
+
return 0;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function trackPendingRequest(model, provider, connectionId, started, error = false) {
|
|
368
|
+
const modelKey = provider ? `${model} (${provider})` : model;
|
|
369
|
+
const timerKey = `${connectionId}|${modelKey}`;
|
|
370
|
+
|
|
371
|
+
if (!pendingRequests.byModel[modelKey]) pendingRequests.byModel[modelKey] = 0;
|
|
372
|
+
pendingRequests.byModel[modelKey] = Math.max(0, pendingRequests.byModel[modelKey] + (started ? 1 : -1));
|
|
373
|
+
if (pendingRequests.byModel[modelKey] === 0) delete pendingRequests.byModel[modelKey];
|
|
374
|
+
|
|
375
|
+
if (connectionId) {
|
|
376
|
+
if (!pendingRequests.byAccount[connectionId]) pendingRequests.byAccount[connectionId] = {};
|
|
377
|
+
if (!pendingRequests.byAccount[connectionId][modelKey]) pendingRequests.byAccount[connectionId][modelKey] = 0;
|
|
378
|
+
pendingRequests.byAccount[connectionId][modelKey] = Math.max(0, pendingRequests.byAccount[connectionId][modelKey] + (started ? 1 : -1));
|
|
379
|
+
if (pendingRequests.byAccount[connectionId][modelKey] === 0) {
|
|
380
|
+
delete pendingRequests.byAccount[connectionId][modelKey];
|
|
381
|
+
if (Object.keys(pendingRequests.byAccount[connectionId]).length === 0) {
|
|
382
|
+
delete pendingRequests.byAccount[connectionId];
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (started) {
|
|
388
|
+
clearTimeout(pendingTimers[timerKey]);
|
|
389
|
+
pendingTimers[timerKey] = setTimeout(() => {
|
|
390
|
+
delete pendingTimers[timerKey];
|
|
391
|
+
if (pendingRequests.byModel[modelKey] > 0) pendingRequests.byModel[modelKey] = 0;
|
|
392
|
+
if (connectionId && pendingRequests.byAccount[connectionId]?.[modelKey] > 0) {
|
|
393
|
+
pendingRequests.byAccount[connectionId][modelKey] = 0;
|
|
394
|
+
}
|
|
395
|
+
scheduleStatsEvent("pending");
|
|
396
|
+
}, PENDING_TIMEOUT_MS);
|
|
397
|
+
} else {
|
|
398
|
+
clearTimeout(pendingTimers[timerKey]);
|
|
399
|
+
delete pendingTimers[timerKey];
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (!started && error && provider) {
|
|
403
|
+
lastErrorProvider.provider = provider.toLowerCase();
|
|
404
|
+
lastErrorProvider.ts = Date.now();
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
scheduleStatsEvent("pending");
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export async function getActiveRequests() {
|
|
411
|
+
const activeRequests = [];
|
|
412
|
+
const connectionMap = await getConnectionMapCached();
|
|
413
|
+
|
|
414
|
+
for (const [connectionId, models] of Object.entries(pendingRequests.byAccount)) {
|
|
415
|
+
for (const [modelKey, count] of Object.entries(models)) {
|
|
416
|
+
if (count > 0) {
|
|
417
|
+
const accountName = connectionMap[connectionId] || `Account ${connectionId.slice(0, 8)}...`;
|
|
418
|
+
const match = modelKey.match(/^(.*) \((.*)\)$/);
|
|
419
|
+
activeRequests.push({
|
|
420
|
+
model: match ? match[1] : modelKey,
|
|
421
|
+
provider: match ? match[2] : "unknown",
|
|
422
|
+
account: accountName, count,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
await flushUsageBuffer();
|
|
429
|
+
await ensureRingInitialized();
|
|
430
|
+
const seen = new Set();
|
|
431
|
+
const recentRequests = [...recentRing.items]
|
|
432
|
+
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
|
|
433
|
+
.map((e) => {
|
|
434
|
+
const t = e.tokens || {};
|
|
435
|
+
return {
|
|
436
|
+
timestamp: e.timestamp, model: e.model, provider: e.provider || "",
|
|
437
|
+
promptTokens: t.prompt_tokens || t.input_tokens || 0,
|
|
438
|
+
completionTokens: t.completion_tokens || t.output_tokens || 0,
|
|
439
|
+
cachedTokens: t.cached_tokens || t.cache_read_input_tokens || 0,
|
|
440
|
+
cacheCreationTokens: t.cache_creation_input_tokens || 0,
|
|
441
|
+
status: e.status || "ok",
|
|
442
|
+
};
|
|
443
|
+
})
|
|
444
|
+
.filter((e) => {
|
|
445
|
+
const minute = e.timestamp ? e.timestamp.slice(0, 16) : "";
|
|
446
|
+
const key = `${e.model}|${e.provider}|${e.promptTokens}|${e.completionTokens}|${minute}`;
|
|
447
|
+
if (seen.has(key)) return false;
|
|
448
|
+
seen.add(key);
|
|
449
|
+
return true;
|
|
450
|
+
})
|
|
451
|
+
.slice(0, 20);
|
|
452
|
+
|
|
453
|
+
const errorProvider = (Date.now() - lastErrorProvider.ts < 10000) ? lastErrorProvider.provider : "";
|
|
454
|
+
return { activeRequests, recentRequests, errorProvider };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const usageBuffer = getWriteBehindBuffer({
|
|
458
|
+
name: "usage",
|
|
459
|
+
flushFn: flushUsageBatch,
|
|
460
|
+
flushMs: 20,
|
|
461
|
+
batchSize: 200,
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
export async function flushUsageBuffer() {
|
|
465
|
+
await usageBuffer.flushImmediate();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async function flushUsageBatch(rows) {
|
|
469
|
+
if (!rows.length) return;
|
|
470
|
+
const db = await getAdapter();
|
|
471
|
+
|
|
472
|
+
const seen = new Set();
|
|
473
|
+
const deduped = [];
|
|
474
|
+
for (const e of rows) {
|
|
475
|
+
const tokens = e.tokens || {};
|
|
476
|
+
const promptTokens = tokens.prompt_tokens || tokens.input_tokens || 0;
|
|
477
|
+
const completionTokens = tokens.completion_tokens || tokens.output_tokens || 0;
|
|
478
|
+
const key = `${e.timestamp}|${e.provider || ""}|${e.model || ""}|${e.connectionId || ""}|${e.apiKey || ""}|${promptTokens}|${completionTokens}`;
|
|
479
|
+
if (seen.has(key)) continue;
|
|
480
|
+
seen.add(key);
|
|
481
|
+
deduped.push(e);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const existingKeys = new Set();
|
|
485
|
+
const timestamps = [...new Set(deduped.map((e) => e.timestamp))];
|
|
486
|
+
const existingRows = [];
|
|
487
|
+
for (const ts of timestamps.slice(0, 64)) {
|
|
488
|
+
existingRows.push(...db.all(
|
|
489
|
+
`SELECT timestamp, provider, model, connectionId, apiKey, promptTokens, completionTokens, id, endpoint FROM usageHistory WHERE timestamp = ?`,
|
|
490
|
+
[ts]
|
|
491
|
+
));
|
|
492
|
+
}
|
|
493
|
+
for (const r of existingRows) {
|
|
494
|
+
existingKeys.add(`${r.timestamp}|${r.provider || ""}|${r.model || ""}|${r.connectionId || ""}|${r.apiKey || ""}|${r.promptTokens}|${r.completionTokens}`);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const dayMap = {};
|
|
498
|
+
const touchedDays = new Set();
|
|
499
|
+
for (const e of deduped) touchedDays.add(getLocalDateKey(e.timestamp));
|
|
500
|
+
for (const dateKey of touchedDays) {
|
|
501
|
+
const dayRow = db.get(`SELECT data FROM usageDaily WHERE dateKey = ?`, [dateKey]);
|
|
502
|
+
dayMap[dateKey] = dayRow ? parseJson(dayRow.data, {}) : {
|
|
503
|
+
requests: 0, promptTokens: 0, completionTokens: 0, cost: 0,
|
|
504
|
+
byProvider: {}, byModel: {}, byAccount: {}, byApiKey: {}, byEndpoint: {},
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
let insertedAny = false;
|
|
509
|
+
let insertedCount = 0;
|
|
510
|
+
db.transaction(() => {
|
|
511
|
+
for (const entry of deduped) {
|
|
512
|
+
const tokens = entry.tokens || {};
|
|
513
|
+
const promptTokens = tokens.prompt_tokens || tokens.input_tokens || 0;
|
|
514
|
+
const completionTokens = tokens.completion_tokens || tokens.output_tokens || 0;
|
|
515
|
+
const key = `${entry.timestamp}|${entry.provider || ""}|${entry.model || ""}|${entry.connectionId || ""}|${entry.apiKey || ""}|${promptTokens}|${completionTokens}`;
|
|
516
|
+
|
|
517
|
+
if (existingKeys.has(key)) {
|
|
518
|
+
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
db.run(
|
|
523
|
+
`INSERT INTO usageHistory(timestamp, provider, model, connectionId, apiKey, endpoint, promptTokens, completionTokens, cost, status, tokens, meta) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
524
|
+
[
|
|
525
|
+
entry.timestamp, entry.provider || null, entry.model || null,
|
|
526
|
+
entry.connectionId || null, entry.apiKey || null, entry.endpoint || null,
|
|
527
|
+
promptTokens, completionTokens, entry.cost || 0, entry.status || "ok",
|
|
528
|
+
stringifyJson(tokens), stringifyJson(entry.meta || {}),
|
|
529
|
+
]
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
const dateKey = getLocalDateKey(entry.timestamp);
|
|
533
|
+
aggregateEntryToDay(dayMap[dateKey], entry);
|
|
534
|
+
existingKeys.add(key);
|
|
535
|
+
insertedAny = true;
|
|
536
|
+
insertedCount++;
|
|
537
|
+
pushToRing(entry);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
for (const dateKey of touchedDays) {
|
|
541
|
+
db.run(`INSERT INTO usageDaily(dateKey, data) VALUES(?, ?) ON CONFLICT(dateKey) DO UPDATE SET data = excluded.data`, [dateKey, stringifyJson(dayMap[dateKey])]);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (insertedCount > 0) {
|
|
545
|
+
const cur = db.get(`SELECT value FROM _meta WHERE key = 'totalRequestsLifetime'`);
|
|
546
|
+
const next = (cur ? parseInt(cur.value, 10) : 0) + insertedCount;
|
|
547
|
+
db.run(`INSERT INTO _meta(key, value) VALUES('totalRequestsLifetime', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value`, [String(next)]);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
pruneUsageHistory(db);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
if (insertedAny) scheduleStatsEvent("update", 250);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export async function clearUsageHistory({ startDate, endDate } = {}) {
|
|
557
|
+
await flushUsageBuffer();
|
|
558
|
+
const { clearRequestDetails } = await import("./requestDetailsRepo.js");
|
|
559
|
+
const db = await getAdapter();
|
|
560
|
+
const partial = Boolean(startDate || endDate);
|
|
561
|
+
const conds = [];
|
|
562
|
+
const params = [];
|
|
563
|
+
if (startDate) {
|
|
564
|
+
conds.push("timestamp >= ?");
|
|
565
|
+
params.push(new Date(startDate).toISOString());
|
|
566
|
+
}
|
|
567
|
+
if (endDate) {
|
|
568
|
+
conds.push("timestamp <= ?");
|
|
569
|
+
params.push(new Date(endDate).toISOString());
|
|
570
|
+
}
|
|
571
|
+
const where = conds.length ? `WHERE ${conds.join(" AND ")}` : "";
|
|
572
|
+
let deletedUsage = 0;
|
|
573
|
+
let deletedDaily = 0;
|
|
574
|
+
|
|
575
|
+
db.transaction(() => {
|
|
576
|
+
deletedUsage = Number(db.get(`SELECT COUNT(*) AS count FROM usageHistory ${where}`, params)?.count || 0);
|
|
577
|
+
if (!partial) {
|
|
578
|
+
deletedDaily = Number(db.get(`SELECT COUNT(*) AS count FROM usageDaily`)?.count || 0);
|
|
579
|
+
db.run(`DELETE FROM usageHistory`);
|
|
580
|
+
db.run(`DELETE FROM usageDaily`);
|
|
581
|
+
db.run(`INSERT INTO _meta(key, value) VALUES('totalRequestsLifetime', '0') ON CONFLICT(key) DO UPDATE SET value = excluded.value`);
|
|
582
|
+
db.run(`INSERT INTO _meta(key, value) VALUES('usageHistoryPrunedTotal', '0') ON CONFLICT(key) DO UPDATE SET value = excluded.value`);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
db.run(`DELETE FROM usageHistory ${where}`, params);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
if (partial && deletedUsage > 0) {
|
|
589
|
+
const beforeDaily = Number(db.get(`SELECT COUNT(*) AS count FROM usageDaily`)?.count || 0);
|
|
590
|
+
reconcileUsageAggregates(db);
|
|
591
|
+
const afterDaily = Number(db.get(`SELECT COUNT(*) AS count FROM usageDaily`)?.count || 0);
|
|
592
|
+
deletedDaily = Math.max(0, beforeDaily - afterDaily);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const deletedDetails = await clearRequestDetails({ startDate, endDate });
|
|
596
|
+
if (!partial) {
|
|
597
|
+
recentRing.items = [];
|
|
598
|
+
recentRing.initialized = true;
|
|
599
|
+
} else {
|
|
600
|
+
const startMs = startDate ? Date.parse(startDate) : Number.NEGATIVE_INFINITY;
|
|
601
|
+
const endMs = endDate ? Date.parse(endDate) : Number.POSITIVE_INFINITY;
|
|
602
|
+
recentRing.items = recentRing.items.filter((entry) => {
|
|
603
|
+
const timestamp = Date.parse(entry.timestamp);
|
|
604
|
+
return !Number.isFinite(timestamp) || timestamp < startMs || timestamp > endMs;
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (!partial || (lastErrorProvider.ts >= (startDate ? Date.parse(startDate) : Number.NEGATIVE_INFINITY) && lastErrorProvider.ts <= (endDate ? Date.parse(endDate) : Number.POSITIVE_INFINITY))) {
|
|
609
|
+
lastErrorProvider.provider = "";
|
|
610
|
+
lastErrorProvider.ts = 0;
|
|
611
|
+
}
|
|
612
|
+
scheduleStatsEvent("update", 0);
|
|
613
|
+
return { usageHistory: deletedUsage, usageDaily: deletedDaily, requestDetails: deletedDetails };
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
export async function saveRequestUsage(entry) {
|
|
617
|
+
try {
|
|
618
|
+
if (!entry.timestamp) entry.timestamp = new Date().toISOString();
|
|
619
|
+
|
|
620
|
+
entry.cost = await calculateCost(entry.provider, entry.model, entry.tokens);
|
|
621
|
+
usageBuffer.push(entry);
|
|
622
|
+
} catch (e) {
|
|
623
|
+
console.error("Failed to save usage stats:", e);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export async function getUsageHistory(filter = {}) {
|
|
628
|
+
await flushUsageBuffer();
|
|
629
|
+
const db = await getAdapter();
|
|
630
|
+
const conds = [];
|
|
631
|
+
const params = [];
|
|
632
|
+
|
|
633
|
+
if (filter.provider) { conds.push("uh.provider = ?"); params.push(filter.provider); }
|
|
634
|
+
if (filter.model) { conds.push("uh.model = ?"); params.push(filter.model); }
|
|
635
|
+
if (filter.apiKeyId) { conds.push("ak.id = ?"); params.push(filter.apiKeyId); }
|
|
636
|
+
if (filter.startDate) { conds.push("uh.timestamp >= ?"); params.push(new Date(filter.startDate).toISOString()); }
|
|
637
|
+
if (filter.endDate) { conds.push("uh.timestamp <= ?"); params.push(new Date(filter.endDate).toISOString()); }
|
|
638
|
+
|
|
639
|
+
const where = conds.length ? `WHERE ${conds.join(" AND ")}` : "";
|
|
640
|
+
|
|
641
|
+
const limit = Math.min(500, Math.max(1, Number(filter.limit) || 50));
|
|
642
|
+
const rows = db.all(
|
|
643
|
+
`SELECT uh.id, uh.timestamp, uh.provider, uh.model, uh.connectionId,
|
|
644
|
+
uh.apiKey, uh.endpoint, uh.cost, uh.status, uh.promptTokens,
|
|
645
|
+
uh.completionTokens, uh.tokens, uh.meta, ak.id AS apiKeyId
|
|
646
|
+
FROM usageHistory uh
|
|
647
|
+
LEFT JOIN apiKeys ak ON ak.key = uh.apiKey
|
|
648
|
+
${where}
|
|
649
|
+
ORDER BY uh.timestamp DESC, uh.id DESC
|
|
650
|
+
LIMIT ?`,
|
|
651
|
+
[...params, limit],
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
return rows.map((r) => {
|
|
655
|
+
const tokens = parseJson(r.tokens, {}) || {};
|
|
656
|
+
const meta = parseJson(r.meta, {}) || {};
|
|
657
|
+
return {
|
|
658
|
+
id: String(r.id),
|
|
659
|
+
timestamp: r.timestamp,
|
|
660
|
+
provider: r.provider,
|
|
661
|
+
model: r.model,
|
|
662
|
+
connectionId: r.connectionId,
|
|
663
|
+
apiKeyId: r.apiKeyId || undefined,
|
|
664
|
+
apiKeyMasked: maskApiKey(r.apiKey),
|
|
665
|
+
endpoint: r.endpoint,
|
|
666
|
+
cost: r.cost,
|
|
667
|
+
promptTokens: Number(r.promptTokens ?? tokens.prompt_tokens ?? tokens.input_tokens ?? 0),
|
|
668
|
+
completionTokens: Number(r.completionTokens ?? tokens.completion_tokens ?? tokens.output_tokens ?? 0),
|
|
669
|
+
cachedTokens: Number(tokens.cached_tokens ?? tokens.cache_read_input_tokens ?? 0),
|
|
670
|
+
cacheCreationTokens: Number(tokens.cache_creation_input_tokens ?? 0),
|
|
671
|
+
status: r.status || "ok",
|
|
672
|
+
tokens,
|
|
673
|
+
latencyMs: finiteLatency(meta.latencyMs),
|
|
674
|
+
error: meta.error || null,
|
|
675
|
+
};
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
export async function getUsageHistoryPage(filter = {}) {
|
|
680
|
+
await flushUsageBuffer();
|
|
681
|
+
const db = await getAdapter();
|
|
682
|
+
const conds = [];
|
|
683
|
+
const params = [];
|
|
684
|
+
if (filter.provider) { conds.push("provider = ?"); params.push(filter.provider); }
|
|
685
|
+
if (filter.model) { conds.push("model = ?"); params.push(filter.model); }
|
|
686
|
+
if (filter.status) { conds.push("status = ?"); params.push(filter.status); }
|
|
687
|
+
if (filter.query) {
|
|
688
|
+
const like = `%${String(filter.query).replace(/[\\%_]/g, "\\$&").slice(0, 200)}%`;
|
|
689
|
+
conds.push("(provider LIKE ? ESCAPE '\\\\' OR model LIKE ? ESCAPE '\\\\' OR status LIKE ? ESCAPE '\\\\' OR connectionId LIKE ? ESCAPE '\\\\')");
|
|
690
|
+
params.push(like, like, like, like);
|
|
691
|
+
}
|
|
692
|
+
if (filter.startDate) { conds.push("timestamp >= ?"); params.push(new Date(filter.startDate).toISOString()); }
|
|
693
|
+
if (filter.endDate) { conds.push("timestamp <= ?"); params.push(new Date(filter.endDate).toISOString()); }
|
|
694
|
+
const where = conds.length ? `WHERE ${conds.join(" AND ")}` : "";
|
|
695
|
+
const totalItems = Number(db.get(`SELECT COUNT(*) AS c FROM usageHistory ${where}`, params)?.c || 0);
|
|
696
|
+
const page = Math.max(1, Number(filter.page) || 1);
|
|
697
|
+
const pageSize = Math.min(100, Math.max(1, Number(filter.pageSize) || 20));
|
|
698
|
+
const totalPages = Math.ceil(totalItems / pageSize);
|
|
699
|
+
const order = filter.sortDir === "asc" ? "ASC" : "DESC";
|
|
700
|
+
const rows = db.all(
|
|
701
|
+
`SELECT id, timestamp, provider, model, connectionId, endpoint, cost, status, promptTokens, completionTokens, tokens, meta FROM usageHistory ${where} ORDER BY timestamp ${order}, id ${order} LIMIT ? OFFSET ?`,
|
|
702
|
+
[...params, pageSize, (page - 1) * pageSize]
|
|
703
|
+
);
|
|
704
|
+
return {
|
|
705
|
+
details: rows.map((r) => ({
|
|
706
|
+
id: String(r.id), timestamp: r.timestamp, provider: r.provider, model: r.model,
|
|
707
|
+
connectionId: r.connectionId, endpoint: r.endpoint, cost: r.cost, status: r.status,
|
|
708
|
+
promptTokens: Number(r.promptTokens ?? 0),
|
|
709
|
+
completionTokens: Number(r.completionTokens ?? 0),
|
|
710
|
+
tokens: parseJson(r.tokens, {}),
|
|
711
|
+
latencyMs: finiteLatency(parseJson(r.meta, {})?.latencyMs),
|
|
712
|
+
error: parseJson(r.meta, {})?.error || null,
|
|
713
|
+
})),
|
|
714
|
+
pagination: { page, pageSize, totalItems, totalPages, hasNext: page < totalPages, hasPrev: page > 1 },
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function loadDaysInRange(adapter, maxDays) {
|
|
719
|
+
if (maxDays == null) {
|
|
720
|
+
return adapter.all(`SELECT dateKey, data FROM usageDaily`);
|
|
721
|
+
}
|
|
722
|
+
const today = new Date();
|
|
723
|
+
const cutoff = new Date(today.getFullYear(), today.getMonth(), today.getDate() - maxDays + 1);
|
|
724
|
+
const cutoffKey = `${cutoff.getFullYear()}-${String(cutoff.getMonth() + 1).padStart(2, "0")}-${String(cutoff.getDate()).padStart(2, "0")}`;
|
|
725
|
+
return adapter.all(`SELECT dateKey, data FROM usageDaily WHERE dateKey >= ?`, [cutoffKey]);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
export async function getUsageStats(period = "all") {
|
|
729
|
+
await flushUsageBuffer();
|
|
730
|
+
const db = await getAdapter();
|
|
731
|
+
reconcileUsageAggregates(db);
|
|
732
|
+
|
|
733
|
+
const [{ getProviderConnectionsForRouting }, { getApiKeys }, { getProviderNodes }] = await Promise.all([
|
|
734
|
+
import("./connectionsRepo.js"),
|
|
735
|
+
import("./apiKeysRepo.js"),
|
|
736
|
+
import("./nodesRepo.js"),
|
|
737
|
+
]);
|
|
738
|
+
|
|
739
|
+
let allConnections = [];
|
|
740
|
+
try { allConnections = await getProviderConnectionsForRouting(); } catch {}
|
|
741
|
+
const connectionMap = {};
|
|
742
|
+
for (const c of allConnections) connectionMap[c.id] = c.name || c.email || c.id;
|
|
743
|
+
|
|
744
|
+
const providerNodeNameMap = {};
|
|
745
|
+
try {
|
|
746
|
+
const nodes = await getProviderNodes();
|
|
747
|
+
for (const n of nodes) if (n.id && n.name) providerNodeNameMap[n.id] = n.name;
|
|
748
|
+
} catch {}
|
|
749
|
+
|
|
750
|
+
let allApiKeys = [];
|
|
751
|
+
try { allApiKeys = await getApiKeys(); } catch {}
|
|
752
|
+
const apiKeyMap = {};
|
|
753
|
+
for (const k of allApiKeys) apiKeyMap[k.key] = { name: k.name, id: k.id, createdAt: k.createdAt };
|
|
754
|
+
|
|
755
|
+
const recentCutoff = periodStart(period);
|
|
756
|
+
const recentRows = db.all(
|
|
757
|
+
`SELECT timestamp, provider, model, connectionId, tokens, meta, status
|
|
758
|
+
FROM usageHistory
|
|
759
|
+
${recentCutoff ? "WHERE timestamp >= ?" : ""}
|
|
760
|
+
ORDER BY id DESC LIMIT 100`,
|
|
761
|
+
recentCutoff ? [recentCutoff] : [],
|
|
762
|
+
);
|
|
763
|
+
const seen = new Set();
|
|
764
|
+
const recentRequests = enrichRecentLatencies(db, recentRows
|
|
765
|
+
.map((r) => {
|
|
766
|
+
const t = parseJson(r.tokens, {}) || {};
|
|
767
|
+
return {
|
|
768
|
+
timestamp: r.timestamp, model: r.model, provider: r.provider || "", connectionId: r.connectionId || null,
|
|
769
|
+
promptTokens: t.prompt_tokens || t.input_tokens || 0,
|
|
770
|
+
completionTokens: t.completion_tokens || t.output_tokens || 0,
|
|
771
|
+
cachedTokens: t.cached_tokens || t.cache_read_input_tokens || 0,
|
|
772
|
+
cacheCreationTokens: t.cache_creation_input_tokens || 0,
|
|
773
|
+
latencyMs: finiteLatency(parseJson(r.meta, {})?.latencyMs),
|
|
774
|
+
status: r.status || "ok",
|
|
775
|
+
};
|
|
776
|
+
})
|
|
777
|
+
.filter((e) => {
|
|
778
|
+
const minute = e.timestamp ? e.timestamp.slice(0, 16) : "";
|
|
779
|
+
const key = `${e.model}|${e.provider}|${e.promptTokens}|${e.completionTokens}|${minute}`;
|
|
780
|
+
if (seen.has(key)) return false;
|
|
781
|
+
seen.add(key);
|
|
782
|
+
return true;
|
|
783
|
+
})
|
|
784
|
+
.slice(0, 20));
|
|
785
|
+
|
|
786
|
+
const stats = {
|
|
787
|
+
totalRequests: 0,
|
|
788
|
+
totalPromptTokens: 0, totalCompletionTokens: 0, totalCachedTokens: 0, totalCost: 0,
|
|
789
|
+
byProvider: {}, byModel: {}, byAccount: {}, byApiKey: {}, byEndpoint: {},
|
|
790
|
+
last10Minutes: [],
|
|
791
|
+
pending: pendingRequests,
|
|
792
|
+
activeRequests: [],
|
|
793
|
+
recentRequests,
|
|
794
|
+
errorProvider: (Date.now() - lastErrorProvider.ts < 10000) ? lastErrorProvider.provider : "",
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
for (const [connectionId, models] of Object.entries(pendingRequests.byAccount)) {
|
|
798
|
+
for (const [modelKey, count] of Object.entries(models)) {
|
|
799
|
+
if (count > 0) {
|
|
800
|
+
const accountName = connectionMap[connectionId] || `Account ${connectionId.slice(0, 8)}...`;
|
|
801
|
+
const match = modelKey.match(/^(.*) \((.*)\)$/);
|
|
802
|
+
stats.activeRequests.push({
|
|
803
|
+
model: match ? match[1] : modelKey,
|
|
804
|
+
provider: match ? match[2] : "unknown",
|
|
805
|
+
account: accountName, count,
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const now = new Date();
|
|
812
|
+
const currentMinuteStart = new Date(Math.floor(now.getTime() / 60000) * 60000);
|
|
813
|
+
const tenMinutesAgo = new Date(currentMinuteStart.getTime() - 9 * 60 * 1000);
|
|
814
|
+
const bucketMap = {};
|
|
815
|
+
for (let i = 0; i < 10; i++) {
|
|
816
|
+
const ts = currentMinuteStart.getTime() - (9 - i) * 60 * 1000;
|
|
817
|
+
bucketMap[ts] = { requests: 0, promptTokens: 0, completionTokens: 0, cost: 0 };
|
|
818
|
+
stats.last10Minutes.push(bucketMap[ts]);
|
|
819
|
+
}
|
|
820
|
+
const recent10 = db.all(
|
|
821
|
+
`SELECT timestamp, promptTokens, completionTokens, cost FROM usageHistory WHERE timestamp >= ? AND timestamp <= ?`,
|
|
822
|
+
[tenMinutesAgo.toISOString(), now.toISOString()]
|
|
823
|
+
);
|
|
824
|
+
for (const r of recent10) {
|
|
825
|
+
const tt = new Date(r.timestamp).getTime();
|
|
826
|
+
const minuteStart = Math.floor(tt / 60000) * 60000;
|
|
827
|
+
if (bucketMap[minuteStart]) {
|
|
828
|
+
bucketMap[minuteStart].requests++;
|
|
829
|
+
bucketMap[minuteStart].promptTokens += r.promptTokens || 0;
|
|
830
|
+
bucketMap[minuteStart].completionTokens += r.completionTokens || 0;
|
|
831
|
+
bucketMap[minuteStart].cost += r.cost || 0;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const useDailySummary = period !== "24h" && period !== "today";
|
|
836
|
+
|
|
837
|
+
if (useDailySummary) {
|
|
838
|
+
const now = new Date();
|
|
839
|
+
const daysInCurrentYear = Math.floor(
|
|
840
|
+
(now.getTime() - new Date(now.getFullYear(), 0, 1).getTime()) / 86400000,
|
|
841
|
+
) + 1;
|
|
842
|
+
const periodDays = { "7d": 7, "30d": 30, "60d": 60, "90d": 90, "365d": 365, year: daysInCurrentYear };
|
|
843
|
+
const maxDays = periodDays[period] || null;
|
|
844
|
+
const dayRows = loadDaysInRange(db, maxDays);
|
|
845
|
+
|
|
846
|
+
for (const dr of dayRows) {
|
|
847
|
+
const dateKey = dr.dateKey;
|
|
848
|
+
const day = parseJson(dr.data, {});
|
|
849
|
+
stats.totalPromptTokens += day.promptTokens || 0;
|
|
850
|
+
stats.totalCompletionTokens += day.completionTokens || 0;
|
|
851
|
+
stats.totalCachedTokens += day.cachedTokens || 0;
|
|
852
|
+
stats.totalCost += day.cost || 0;
|
|
853
|
+
|
|
854
|
+
for (const [prov, p] of Object.entries(day.byProvider || {})) {
|
|
855
|
+
if (!stats.byProvider[prov]) stats.byProvider[prov] = { requests: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cost: 0 };
|
|
856
|
+
stats.byProvider[prov].requests += p.requests || 0;
|
|
857
|
+
stats.byProvider[prov].promptTokens += p.promptTokens || 0;
|
|
858
|
+
stats.byProvider[prov].completionTokens += p.completionTokens || 0;
|
|
859
|
+
stats.byProvider[prov].cachedTokens += p.cachedTokens || 0;
|
|
860
|
+
stats.byProvider[prov].cost += p.cost || 0;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
for (const [mk, m] of Object.entries(day.byModel || {})) {
|
|
864
|
+
const rawModel = m.rawModel || mk.split("|")[0];
|
|
865
|
+
const provider = m.provider || mk.split("|")[1] || "";
|
|
866
|
+
const statsKey = provider ? `${rawModel} (${provider})` : rawModel;
|
|
867
|
+
const providerDisplayName = providerNodeNameMap[provider] || provider;
|
|
868
|
+
if (!stats.byModel[statsKey]) {
|
|
869
|
+
stats.byModel[statsKey] = { requests: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cost: 0, rawModel, provider: providerDisplayName, lastUsed: dateKey };
|
|
870
|
+
}
|
|
871
|
+
stats.byModel[statsKey].requests += m.requests || 0;
|
|
872
|
+
stats.byModel[statsKey].promptTokens += m.promptTokens || 0;
|
|
873
|
+
stats.byModel[statsKey].completionTokens += m.completionTokens || 0;
|
|
874
|
+
stats.byModel[statsKey].cachedTokens += m.cachedTokens || 0;
|
|
875
|
+
stats.byModel[statsKey].cost += m.cost || 0;
|
|
876
|
+
if (dateKey > (stats.byModel[statsKey].lastUsed || "")) stats.byModel[statsKey].lastUsed = dateKey;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
for (const [connId, a] of Object.entries(day.byAccount || {})) {
|
|
880
|
+
const accountName = connectionMap[connId] || `Account ${connId.slice(0, 8)}...`;
|
|
881
|
+
const rawModel = a.rawModel || "";
|
|
882
|
+
const provider = a.provider || "";
|
|
883
|
+
const providerDisplayName = providerNodeNameMap[provider] || provider;
|
|
884
|
+
const accountKey = `${rawModel} (${provider} - ${accountName})`;
|
|
885
|
+
if (!stats.byAccount[accountKey]) {
|
|
886
|
+
stats.byAccount[accountKey] = { requests: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cost: 0, rawModel, provider: providerDisplayName, connectionId: connId, accountName, lastUsed: dateKey };
|
|
887
|
+
}
|
|
888
|
+
stats.byAccount[accountKey].requests += a.requests || 0;
|
|
889
|
+
stats.byAccount[accountKey].promptTokens += a.promptTokens || 0;
|
|
890
|
+
stats.byAccount[accountKey].completionTokens += a.completionTokens || 0;
|
|
891
|
+
stats.byAccount[accountKey].cachedTokens += a.cachedTokens || 0;
|
|
892
|
+
stats.byAccount[accountKey].cost += a.cost || 0;
|
|
893
|
+
if (dateKey > (stats.byAccount[accountKey].lastUsed || "")) stats.byAccount[accountKey].lastUsed = dateKey;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
for (const [akKey, ak] of Object.entries(day.byApiKey || {})) {
|
|
897
|
+
const rawModel = ak.rawModel || "";
|
|
898
|
+
const provider = ak.provider || "";
|
|
899
|
+
const providerDisplayName = providerNodeNameMap[provider] || provider;
|
|
900
|
+
const apiKeyVal = ak.apiKey;
|
|
901
|
+
const keyInfo = apiKeyVal ? apiKeyMap[apiKeyVal] : null;
|
|
902
|
+
const keyName = keyInfo?.name || (apiKeyVal ? apiKeyVal.slice(0, 8) + "..." : "Local (No API Key)");
|
|
903
|
+
const apiKeyMasked = maskApiKey(apiKeyVal);
|
|
904
|
+
const apiKeyKey = apiKeyMasked || "local-no-key";
|
|
905
|
+
if (!stats.byApiKey[akKey]) {
|
|
906
|
+
stats.byApiKey[akKey] = { requests: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cost: 0, rawModel, provider: providerDisplayName, apiKeyMasked, keyName, apiKeyKey, lastUsed: dateKey };
|
|
907
|
+
}
|
|
908
|
+
stats.byApiKey[akKey].requests += ak.requests || 0;
|
|
909
|
+
stats.byApiKey[akKey].promptTokens += ak.promptTokens || 0;
|
|
910
|
+
stats.byApiKey[akKey].completionTokens += ak.completionTokens || 0;
|
|
911
|
+
stats.byApiKey[akKey].cachedTokens += ak.cachedTokens || 0;
|
|
912
|
+
stats.byApiKey[akKey].cost += ak.cost || 0;
|
|
913
|
+
if (dateKey > (stats.byApiKey[akKey].lastUsed || "")) stats.byApiKey[akKey].lastUsed = dateKey;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
for (const [epKey, ep] of Object.entries(day.byEndpoint || {})) {
|
|
917
|
+
const endpoint = ep.endpoint || epKey.split("|")[0] || "Unknown";
|
|
918
|
+
const rawModel = ep.rawModel || "";
|
|
919
|
+
const provider = ep.provider || "";
|
|
920
|
+
const providerDisplayName = providerNodeNameMap[provider] || provider;
|
|
921
|
+
if (!stats.byEndpoint[epKey]) {
|
|
922
|
+
stats.byEndpoint[epKey] = { requests: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cost: 0, endpoint, rawModel, provider: providerDisplayName, lastUsed: dateKey };
|
|
923
|
+
}
|
|
924
|
+
stats.byEndpoint[epKey].requests += ep.requests || 0;
|
|
925
|
+
stats.byEndpoint[epKey].promptTokens += ep.promptTokens || 0;
|
|
926
|
+
stats.byEndpoint[epKey].completionTokens += ep.completionTokens || 0;
|
|
927
|
+
stats.byEndpoint[epKey].cachedTokens += ep.cachedTokens || 0;
|
|
928
|
+
stats.byEndpoint[epKey].cost += ep.cost || 0;
|
|
929
|
+
if (dateKey > (stats.byEndpoint[epKey].lastUsed || "")) stats.byEndpoint[epKey].lastUsed = dateKey;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const overlayCutoff = maxDays ? Date.now() - maxDays * 86400000 : 0;
|
|
934
|
+
const histRows = db.all(
|
|
935
|
+
`SELECT timestamp, provider, model, connectionId, apiKey, endpoint FROM usageHistory WHERE timestamp >= ?`,
|
|
936
|
+
[new Date(overlayCutoff).toISOString()]
|
|
937
|
+
);
|
|
938
|
+
for (const e of histRows) {
|
|
939
|
+
const ts = e.timestamp;
|
|
940
|
+
const modelKey = e.provider ? `${e.model} (${e.provider})` : e.model;
|
|
941
|
+
if (stats.byModel[modelKey] && new Date(ts) > new Date(stats.byModel[modelKey].lastUsed)) stats.byModel[modelKey].lastUsed = ts;
|
|
942
|
+
|
|
943
|
+
if (e.connectionId) {
|
|
944
|
+
const accountName = connectionMap[e.connectionId] || `Account ${e.connectionId.slice(0, 8)}...`;
|
|
945
|
+
const accountKey = `${e.model} (${e.provider} - ${accountName})`;
|
|
946
|
+
if (stats.byAccount[accountKey] && new Date(ts) > new Date(stats.byAccount[accountKey].lastUsed)) stats.byAccount[accountKey].lastUsed = ts;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const apiKeyKey = (e.apiKey && typeof e.apiKey === "string")
|
|
950
|
+
? `${e.apiKey}|${e.model}|${e.provider || "unknown"}`
|
|
951
|
+
: "local-no-key";
|
|
952
|
+
if (stats.byApiKey[apiKeyKey] && new Date(ts) > new Date(stats.byApiKey[apiKeyKey].lastUsed)) stats.byApiKey[apiKeyKey].lastUsed = ts;
|
|
953
|
+
|
|
954
|
+
const endpoint = e.endpoint || "Unknown";
|
|
955
|
+
const endpointKey = `${endpoint}|${e.model}|${e.provider || "unknown"}`;
|
|
956
|
+
if (stats.byEndpoint[endpointKey] && new Date(ts) > new Date(stats.byEndpoint[endpointKey].lastUsed)) stats.byEndpoint[endpointKey].lastUsed = ts;
|
|
957
|
+
}
|
|
958
|
+
} else {
|
|
959
|
+
|
|
960
|
+
let cutoff;
|
|
961
|
+
if (period === "today") {
|
|
962
|
+
const startOfDay = new Date();
|
|
963
|
+
startOfDay.setHours(0, 0, 0, 0);
|
|
964
|
+
cutoff = startOfDay.toISOString();
|
|
965
|
+
} else {
|
|
966
|
+
cutoff = new Date(Date.now() - PERIOD_MS["24h"]).toISOString();
|
|
967
|
+
}
|
|
968
|
+
const filtered = db.all(
|
|
969
|
+
`SELECT timestamp, provider, model, connectionId, apiKey, endpoint, promptTokens, completionTokens, cost, tokens FROM usageHistory WHERE timestamp >= ?`,
|
|
970
|
+
[cutoff]
|
|
971
|
+
);
|
|
972
|
+
|
|
973
|
+
for (const r of filtered) {
|
|
974
|
+
const tokens = parseJson(r.tokens, {}) || {};
|
|
975
|
+
const promptTokens = tokens.prompt_tokens || 0;
|
|
976
|
+
const completionTokens = tokens.completion_tokens || 0;
|
|
977
|
+
const cachedTokens = tokens.cached_tokens || tokens.cache_read_input_tokens || 0;
|
|
978
|
+
const entryCost = r.cost || 0;
|
|
979
|
+
const providerDisplayName = providerNodeNameMap[r.provider] || r.provider;
|
|
980
|
+
|
|
981
|
+
stats.totalPromptTokens += promptTokens;
|
|
982
|
+
stats.totalCompletionTokens += completionTokens;
|
|
983
|
+
stats.totalCachedTokens += cachedTokens;
|
|
984
|
+
stats.totalCost += entryCost;
|
|
985
|
+
|
|
986
|
+
if (!stats.byProvider[r.provider]) stats.byProvider[r.provider] = { requests: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cost: 0 };
|
|
987
|
+
stats.byProvider[r.provider].requests++;
|
|
988
|
+
stats.byProvider[r.provider].promptTokens += promptTokens;
|
|
989
|
+
stats.byProvider[r.provider].completionTokens += completionTokens;
|
|
990
|
+
stats.byProvider[r.provider].cachedTokens += cachedTokens;
|
|
991
|
+
stats.byProvider[r.provider].cost += entryCost;
|
|
992
|
+
|
|
993
|
+
const modelKey = r.provider ? `${r.model} (${r.provider})` : r.model;
|
|
994
|
+
if (!stats.byModel[modelKey]) {
|
|
995
|
+
stats.byModel[modelKey] = { requests: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cost: 0, rawModel: r.model, provider: providerDisplayName, lastUsed: r.timestamp };
|
|
996
|
+
}
|
|
997
|
+
stats.byModel[modelKey].requests++;
|
|
998
|
+
stats.byModel[modelKey].promptTokens += promptTokens;
|
|
999
|
+
stats.byModel[modelKey].completionTokens += completionTokens;
|
|
1000
|
+
stats.byModel[modelKey].cachedTokens += cachedTokens;
|
|
1001
|
+
stats.byModel[modelKey].cost += entryCost;
|
|
1002
|
+
if (new Date(r.timestamp) > new Date(stats.byModel[modelKey].lastUsed)) stats.byModel[modelKey].lastUsed = r.timestamp;
|
|
1003
|
+
|
|
1004
|
+
if (r.connectionId) {
|
|
1005
|
+
const accountName = connectionMap[r.connectionId] || `Account ${r.connectionId.slice(0, 8)}...`;
|
|
1006
|
+
const accountKey = `${r.model} (${r.provider} - ${accountName})`;
|
|
1007
|
+
if (!stats.byAccount[accountKey]) {
|
|
1008
|
+
stats.byAccount[accountKey] = { requests: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cost: 0, rawModel: r.model, provider: providerDisplayName, connectionId: r.connectionId, accountName, lastUsed: r.timestamp };
|
|
1009
|
+
}
|
|
1010
|
+
stats.byAccount[accountKey].requests++;
|
|
1011
|
+
stats.byAccount[accountKey].promptTokens += promptTokens;
|
|
1012
|
+
stats.byAccount[accountKey].completionTokens += completionTokens;
|
|
1013
|
+
stats.byAccount[accountKey].cachedTokens += cachedTokens;
|
|
1014
|
+
stats.byAccount[accountKey].cost += entryCost;
|
|
1015
|
+
if (new Date(r.timestamp) > new Date(stats.byAccount[accountKey].lastUsed)) stats.byAccount[accountKey].lastUsed = r.timestamp;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
if (r.apiKey && typeof r.apiKey === "string") {
|
|
1019
|
+
const keyInfo = apiKeyMap[r.apiKey];
|
|
1020
|
+
const keyName = keyInfo?.name || r.apiKey.slice(0, 8) + "...";
|
|
1021
|
+
const apiKeyMasked = maskApiKey(r.apiKey);
|
|
1022
|
+
const akKey = `${apiKeyMasked}|${r.model}|${r.provider || "unknown"}`;
|
|
1023
|
+
if (!stats.byApiKey[akKey]) {
|
|
1024
|
+
stats.byApiKey[akKey] = { requests: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cost: 0, rawModel: r.model, provider: providerDisplayName, apiKeyMasked, keyName, apiKeyKey: apiKeyMasked, lastUsed: r.timestamp };
|
|
1025
|
+
}
|
|
1026
|
+
const ake = stats.byApiKey[akKey];
|
|
1027
|
+
ake.requests++; ake.promptTokens += promptTokens; ake.completionTokens += completionTokens; ake.cachedTokens += cachedTokens; ake.cost += entryCost;
|
|
1028
|
+
if (new Date(r.timestamp) > new Date(ake.lastUsed)) ake.lastUsed = r.timestamp;
|
|
1029
|
+
} else {
|
|
1030
|
+
if (!stats.byApiKey["local-no-key"]) {
|
|
1031
|
+
stats.byApiKey["local-no-key"] = { requests: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cost: 0, rawModel: r.model, provider: providerDisplayName, apiKeyMasked: null, keyName: "Local (No API Key)", apiKeyKey: "local-no-key", lastUsed: r.timestamp };
|
|
1032
|
+
}
|
|
1033
|
+
const ake = stats.byApiKey["local-no-key"];
|
|
1034
|
+
ake.requests++; ake.promptTokens += promptTokens; ake.completionTokens += completionTokens; ake.cachedTokens += cachedTokens; ake.cost += entryCost;
|
|
1035
|
+
if (new Date(r.timestamp) > new Date(ake.lastUsed)) ake.lastUsed = r.timestamp;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
const endpoint = r.endpoint || "Unknown";
|
|
1039
|
+
const epKey = `${endpoint}|${r.model}|${r.provider || "unknown"}`;
|
|
1040
|
+
if (!stats.byEndpoint[epKey]) {
|
|
1041
|
+
stats.byEndpoint[epKey] = { requests: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cost: 0, endpoint, rawModel: r.model, provider: providerDisplayName, lastUsed: r.timestamp };
|
|
1042
|
+
}
|
|
1043
|
+
const epe = stats.byEndpoint[epKey];
|
|
1044
|
+
epe.requests++; epe.promptTokens += promptTokens; epe.completionTokens += completionTokens; epe.cachedTokens += cachedTokens; epe.cost += entryCost;
|
|
1045
|
+
if (new Date(r.timestamp) > new Date(epe.lastUsed)) epe.lastUsed = r.timestamp;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
stats.totalRequests = Object.values(stats.byProvider).reduce((sum, p) => sum + (p.requests || 0), 0);
|
|
1050
|
+
return stats;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function getHourlyHeatmapData(db, days = 30) {
|
|
1054
|
+
const today = new Date();
|
|
1055
|
+
today.setHours(0, 0, 0, 0);
|
|
1056
|
+
|
|
1057
|
+
const start = new Date(today);
|
|
1058
|
+
start.setDate(start.getDate() - (days - 1));
|
|
1059
|
+
|
|
1060
|
+
const heatmap = Array.from({ length: days }, (_, index) => {
|
|
1061
|
+
const date = new Date(start);
|
|
1062
|
+
date.setDate(start.getDate() + index);
|
|
1063
|
+
const dateKey = getLocalDateKey(date);
|
|
1064
|
+
return {
|
|
1065
|
+
date: dateKey,
|
|
1066
|
+
hours: Array.from({ length: 24 }, (_, hour) => ({
|
|
1067
|
+
hour,
|
|
1068
|
+
requests: 0,
|
|
1069
|
+
errors: 0,
|
|
1070
|
+
tokens: 0,
|
|
1071
|
+
cost: 0,
|
|
1072
|
+
})),
|
|
1073
|
+
};
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
const byDate = new Map(heatmap.map((day) => [day.date, day]));
|
|
1077
|
+
const rows = db.all(
|
|
1078
|
+
`SELECT timestamp, promptTokens, completionTokens, cost, status, tokens, meta
|
|
1079
|
+
FROM usageHistory
|
|
1080
|
+
WHERE timestamp >= ?`,
|
|
1081
|
+
[start.toISOString()],
|
|
1082
|
+
);
|
|
1083
|
+
|
|
1084
|
+
for (const row of rows) {
|
|
1085
|
+
const timestamp = new Date(row.timestamp);
|
|
1086
|
+
if (Number.isNaN(timestamp.getTime())) continue;
|
|
1087
|
+
|
|
1088
|
+
const day = byDate.get(getLocalDateKey(row.timestamp));
|
|
1089
|
+
const hour = timestamp.getHours();
|
|
1090
|
+
if (!day || hour < 0 || hour > 23) continue;
|
|
1091
|
+
|
|
1092
|
+
const cell = day.hours[hour];
|
|
1093
|
+
const tokenData = row.tokens ? parseJson(row.tokens, {}) : {};
|
|
1094
|
+
const metaData = row.meta ? parseJson(row.meta, {}) : {};
|
|
1095
|
+
const promptTokens = Number(row.promptTokens) || Number(tokenData.prompt_tokens ?? tokenData.input_tokens ?? 0) || 0;
|
|
1096
|
+
const completionTokens = Number(row.completionTokens) || Number(tokenData.completion_tokens ?? tokenData.output_tokens ?? 0) || 0;
|
|
1097
|
+
cell.requests += 1;
|
|
1098
|
+
cell.errors += isUsageErrorStatus(row.status) || Boolean(metaData?.error) ? 1 : 0;
|
|
1099
|
+
cell.tokens += promptTokens + completionTokens;
|
|
1100
|
+
cell.cost += Number(row.cost) || 0;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
return heatmap;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
export async function getChartData(period = "7d", options = {}) {
|
|
1107
|
+
await flushUsageBuffer();
|
|
1108
|
+
const db = await getAdapter();
|
|
1109
|
+
reconcileUsageAggregates(db);
|
|
1110
|
+
const now = Date.now();
|
|
1111
|
+
|
|
1112
|
+
if (options.granularity === "hour" && (period === "7d" || period === "30d")) {
|
|
1113
|
+
const days = Number(options.days) || (period === "7d" ? 7 : 30);
|
|
1114
|
+
return getHourlyHeatmapData(db, Math.min(Math.max(days, 1), 30));
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
if (period === "today") {
|
|
1118
|
+
const bucketCount = 24;
|
|
1119
|
+
const bucketMs = 3600000;
|
|
1120
|
+
const startOfDay = new Date();
|
|
1121
|
+
startOfDay.setHours(0, 0, 0, 0);
|
|
1122
|
+
const startTime = startOfDay.getTime();
|
|
1123
|
+
const endTime = startTime + bucketCount * bucketMs;
|
|
1124
|
+
const labelFn = (ts) => new Date(ts).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false });
|
|
1125
|
+
const buckets = Array.from({ length: bucketCount }, (_, i) => ({ label: labelFn(startTime + i * bucketMs), tokens: 0, cost: 0, requests: 0 }));
|
|
1126
|
+
|
|
1127
|
+
const rows = db.all(
|
|
1128
|
+
`SELECT timestamp, promptTokens, completionTokens, cost FROM usageHistory WHERE timestamp >= ?`,
|
|
1129
|
+
[new Date(startTime).toISOString()]
|
|
1130
|
+
);
|
|
1131
|
+
for (const r of rows) {
|
|
1132
|
+
const t = new Date(r.timestamp).getTime();
|
|
1133
|
+
if (t < startTime || t >= endTime) continue;
|
|
1134
|
+
const idx = Math.floor((t - startTime) / bucketMs);
|
|
1135
|
+
if (idx >= 0 && idx < bucketCount) {
|
|
1136
|
+
buckets[idx].tokens += (r.promptTokens || 0) + (r.completionTokens || 0);
|
|
1137
|
+
buckets[idx].cost += r.cost || 0;
|
|
1138
|
+
buckets[idx].requests++;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
return buckets;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
if (period === "24h") {
|
|
1145
|
+
const bucketCount = 24;
|
|
1146
|
+
const bucketMs = 3600000;
|
|
1147
|
+
const labelFn = (ts) => new Date(ts).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false });
|
|
1148
|
+
const startTime = now - bucketCount * bucketMs;
|
|
1149
|
+
const buckets = Array.from({ length: bucketCount }, (_, i) => ({ label: labelFn(startTime + i * bucketMs), tokens: 0, cost: 0, requests: 0 }));
|
|
1150
|
+
|
|
1151
|
+
const rows = db.all(
|
|
1152
|
+
`SELECT timestamp, promptTokens, completionTokens, cost FROM usageHistory WHERE timestamp >= ?`,
|
|
1153
|
+
[new Date(startTime).toISOString()]
|
|
1154
|
+
);
|
|
1155
|
+
for (const r of rows) {
|
|
1156
|
+
const t = new Date(r.timestamp).getTime();
|
|
1157
|
+
if (t < startTime || t > now) continue;
|
|
1158
|
+
const idx = Math.min(Math.floor((t - startTime) / bucketMs), bucketCount - 1);
|
|
1159
|
+
buckets[idx].tokens += (r.promptTokens || 0) + (r.completionTokens || 0);
|
|
1160
|
+
buckets[idx].cost += r.cost || 0;
|
|
1161
|
+
buckets[idx].requests++;
|
|
1162
|
+
}
|
|
1163
|
+
return buckets;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
const today = new Date();
|
|
1167
|
+
const calendarYear = today.getFullYear();
|
|
1168
|
+
const isCalendarYear = period === "year";
|
|
1169
|
+
const bucketCount = isCalendarYear
|
|
1170
|
+
? Math.round((Date.UTC(calendarYear + 1, 0, 1) - Date.UTC(calendarYear, 0, 1)) / 86400000)
|
|
1171
|
+
: period === "7d" ? 7 : period === "30d" ? 30 : period === "90d" ? 90 : period === "180d" ? 180 : period === "365d" ? 365 : 60;
|
|
1172
|
+
const labelFn = (d) => d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
1173
|
+
|
|
1174
|
+
const calendarStartKey = `${calendarYear}-01-01`;
|
|
1175
|
+
const calendarEndKey = `${calendarYear}-12-31`;
|
|
1176
|
+
const dayRows = isCalendarYear
|
|
1177
|
+
? db.all(`SELECT dateKey, data FROM usageDaily WHERE dateKey >= ? AND dateKey <= ?`, [calendarStartKey, calendarEndKey])
|
|
1178
|
+
: loadDaysInRange(db, bucketCount);
|
|
1179
|
+
const dayMap = {};
|
|
1180
|
+
for (const r of dayRows) dayMap[r.dateKey] = parseJson(r.data, {});
|
|
1181
|
+
|
|
1182
|
+
const firstDay = isCalendarYear ? new Date(calendarYear, 0, 1) : new Date(today);
|
|
1183
|
+
firstDay.setHours(0, 0, 0, 0);
|
|
1184
|
+
if (!isCalendarYear) firstDay.setDate(firstDay.getDate() - (bucketCount - 1));
|
|
1185
|
+
const errorRows = isCalendarYear
|
|
1186
|
+
? db.all(
|
|
1187
|
+
`SELECT timestamp, status FROM usageHistory WHERE timestamp >= ? AND timestamp < ?`,
|
|
1188
|
+
[firstDay.toISOString(), new Date(calendarYear + 1, 0, 1).toISOString()],
|
|
1189
|
+
)
|
|
1190
|
+
: db.all(
|
|
1191
|
+
`SELECT timestamp, status FROM usageHistory WHERE timestamp >= ?`,
|
|
1192
|
+
[firstDay.toISOString()],
|
|
1193
|
+
);
|
|
1194
|
+
const errorsByDate = {};
|
|
1195
|
+
for (const row of errorRows) {
|
|
1196
|
+
const status = String(row.status || "").toLowerCase();
|
|
1197
|
+
if (!isUsageErrorStatus(status)) continue;
|
|
1198
|
+
const date = new Date(row.timestamp);
|
|
1199
|
+
if (Number.isNaN(date.getTime())) continue;
|
|
1200
|
+
const key = getLocalDateKey(row.timestamp);
|
|
1201
|
+
errorsByDate[key] = (errorsByDate[key] || 0) + 1;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
return Array.from({ length: bucketCount }, (_, i) => {
|
|
1205
|
+
const d = isCalendarYear ? new Date(calendarYear, 0, 1) : new Date(today);
|
|
1206
|
+
if (isCalendarYear) d.setDate(d.getDate() + i);
|
|
1207
|
+
else d.setDate(d.getDate() - (bucketCount - 1 - i));
|
|
1208
|
+
const dateKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
1209
|
+
const dayData = dayMap[dateKey];
|
|
1210
|
+
return {
|
|
1211
|
+
date: dateKey,
|
|
1212
|
+
label: labelFn(d),
|
|
1213
|
+
tokens: dayData ? (dayData.promptTokens || 0) + (dayData.completionTokens || 0) : 0,
|
|
1214
|
+
promptTokens: dayData ? dayData.promptTokens || 0 : 0,
|
|
1215
|
+
completionTokens: dayData ? dayData.completionTokens || 0 : 0,
|
|
1216
|
+
cachedTokens: dayData ? dayData.cachedTokens || 0 : 0,
|
|
1217
|
+
cost: dayData ? (dayData.cost || 0) : 0,
|
|
1218
|
+
requests: dayData ? dayData.requests || 0 : 0,
|
|
1219
|
+
errors: errorsByDate[dateKey] || 0,
|
|
1220
|
+
};
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function formatLogDate(date = new Date()) {
|
|
1225
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1226
|
+
return `${pad(date.getDate())}-${pad(date.getMonth() + 1)}-${date.getFullYear()} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
export async function appendRequestLog() {}
|
|
1230
|
+
|
|
1231
|
+
export async function getRecentLogs(limit = 200) {
|
|
1232
|
+
try {
|
|
1233
|
+
await flushUsageBuffer();
|
|
1234
|
+
const db = await getAdapter();
|
|
1235
|
+
const rows = db.all(
|
|
1236
|
+
`SELECT timestamp, provider, model, connectionId, promptTokens, completionTokens, status, tokens FROM usageHistory ORDER BY id DESC LIMIT ?`,
|
|
1237
|
+
[limit],
|
|
1238
|
+
);
|
|
1239
|
+
if (!rows.length) return [];
|
|
1240
|
+
|
|
1241
|
+
const connMap = {};
|
|
1242
|
+
try {
|
|
1243
|
+
const { getProviderConnectionsForRouting } = await import("./connectionsRepo.js");
|
|
1244
|
+
const connections = await getProviderConnectionsForRouting();
|
|
1245
|
+
for (const c of connections) connMap[c.id] = c.name || c.email || "";
|
|
1246
|
+
} catch {}
|
|
1247
|
+
|
|
1248
|
+
return rows.map((r) => {
|
|
1249
|
+
const ts = formatLogDate(new Date(r.timestamp));
|
|
1250
|
+
const p = r.provider?.toUpperCase() || "-";
|
|
1251
|
+
const m = r.model || "-";
|
|
1252
|
+
const account = connMap[r.connectionId] || (r.connectionId ? r.connectionId.slice(0, 8) : "-");
|
|
1253
|
+
const tk = r.tokens ? parseJson(r.tokens, {}) : {};
|
|
1254
|
+
const sent = r.promptTokens ?? tk.prompt_tokens ?? "-";
|
|
1255
|
+
const received = r.completionTokens ?? tk.completion_tokens ?? "-";
|
|
1256
|
+
return `${ts} | ${m} | ${p} | ${account} | ${sent} | ${received} | ${r.status || "-"}`;
|
|
1257
|
+
});
|
|
1258
|
+
} catch (e) {
|
|
1259
|
+
console.error("[usageRepo] getRecentLogs failed:", e.message);
|
|
1260
|
+
return [];
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
export async function searchLogs(opts = {}) {
|
|
1265
|
+
try {
|
|
1266
|
+
await flushUsageBuffer();
|
|
1267
|
+
const db = await getAdapter();
|
|
1268
|
+
|
|
1269
|
+
const page = Math.max(1, parseInt(opts.page, 10) || 1);
|
|
1270
|
+
const limit = Math.min(200, Math.max(1, parseInt(opts.limit, 10) || 50));
|
|
1271
|
+
const offset = (page - 1) * limit;
|
|
1272
|
+
|
|
1273
|
+
const conditions = [];
|
|
1274
|
+
const params = [];
|
|
1275
|
+
|
|
1276
|
+
if (opts.provider) {
|
|
1277
|
+
conditions.push("LOWER(uh.provider) = ?");
|
|
1278
|
+
params.push(opts.provider.toLowerCase());
|
|
1279
|
+
}
|
|
1280
|
+
if (opts.model) {
|
|
1281
|
+
conditions.push("LOWER(uh.model) LIKE ?");
|
|
1282
|
+
params.push(`%${opts.model.toLowerCase()}%`);
|
|
1283
|
+
}
|
|
1284
|
+
if (opts.status) {
|
|
1285
|
+
conditions.push("LOWER(uh.status) = ?");
|
|
1286
|
+
params.push(opts.status.toLowerCase());
|
|
1287
|
+
}
|
|
1288
|
+
if (opts.q) {
|
|
1289
|
+
|
|
1290
|
+
const q = `%${opts.q.toLowerCase()}%`;
|
|
1291
|
+
conditions.push("(LOWER(uh.model) LIKE ? OR LOWER(uh.provider) LIKE ? OR LOWER(COALESCE(pc.name, pc.email, uh.connectionId, '')) LIKE ?)");
|
|
1292
|
+
params.push(q, q, q);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1296
|
+
|
|
1297
|
+
const countRow = db.get(
|
|
1298
|
+
`SELECT COUNT(*) AS cnt FROM usageHistory uh LEFT JOIN providerConnections pc ON uh.connectionId = pc.id ${where}`,
|
|
1299
|
+
params,
|
|
1300
|
+
);
|
|
1301
|
+
const total = countRow?.cnt ?? 0;
|
|
1302
|
+
|
|
1303
|
+
const rows = db.all(
|
|
1304
|
+
`SELECT uh.id, uh.timestamp, uh.provider, uh.model, uh.connectionId,
|
|
1305
|
+
uh.promptTokens, uh.completionTokens, uh.cost, uh.status, uh.tokens,
|
|
1306
|
+
pc.name AS connectionName, pc.email AS connectionEmail
|
|
1307
|
+
FROM usageHistory uh
|
|
1308
|
+
LEFT JOIN providerConnections pc ON uh.connectionId = pc.id
|
|
1309
|
+
${where}
|
|
1310
|
+
ORDER BY uh.id DESC
|
|
1311
|
+
LIMIT ? OFFSET ?`,
|
|
1312
|
+
[...params, limit, offset],
|
|
1313
|
+
);
|
|
1314
|
+
|
|
1315
|
+
const mapped = rows.map((r) => {
|
|
1316
|
+
const tk = r.tokens ? parseJson(r.tokens, {}) : {};
|
|
1317
|
+
return {
|
|
1318
|
+
id: r.id,
|
|
1319
|
+
timestamp: r.timestamp,
|
|
1320
|
+
provider: r.provider || "-",
|
|
1321
|
+
model: r.model || "-",
|
|
1322
|
+
connection: r.connectionName || r.connectionEmail || (r.connectionId ? r.connectionId.slice(0, 8) : "-"),
|
|
1323
|
+
promptTokens: r.promptTokens ?? tk.prompt_tokens ?? 0,
|
|
1324
|
+
completionTokens: r.completionTokens ?? tk.completion_tokens ?? 0,
|
|
1325
|
+
cost: r.cost ?? 0,
|
|
1326
|
+
status: r.status || "-",
|
|
1327
|
+
};
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
return { rows: mapped, total, page, limit };
|
|
1331
|
+
} catch (e) {
|
|
1332
|
+
console.error("[usageRepo] searchLogs failed:", e.message);
|
|
1333
|
+
return { rows: [], total: 0, page: 1, limit: 50 };
|
|
1334
|
+
}
|
|
1335
|
+
}
|