@flame0510/project-aether 1.1.9
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/README.md +150 -0
- package/agent-templates/README.md +37 -0
- package/agent-templates/argus/AGENTS.md +52 -0
- package/agent-templates/argus/IDENTITY.md +7 -0
- package/agent-templates/argus/MEMORY.md +9 -0
- package/agent-templates/argus/SOUL.md +24 -0
- package/agent-templates/argus/TOOLS.md +8 -0
- package/agent-templates/atlas/AGENTS.md +47 -0
- package/agent-templates/atlas/HEARTBEAT.md +10 -0
- package/agent-templates/atlas/IDENTITY.md +6 -0
- package/agent-templates/atlas/MEMORY.md +9 -0
- package/agent-templates/atlas/SOUL.md +21 -0
- package/agent-templates/atlas/TOOLS.md +4 -0
- package/agent-templates/base-image/.trigger +0 -0
- package/agent-templates/base-image/Dockerfile +90 -0
- package/agent-templates/base-image/entrypoint.sh +37 -0
- package/agent-templates/custom/AGENTS.md +29 -0
- package/agent-templates/custom/BOOTSTRAP.md +56 -0
- package/agent-templates/custom/MEMORY.md +8 -0
- package/agent-templates/prometheus/AGENTS.md +37 -0
- package/agent-templates/prometheus/IDENTITY.md +6 -0
- package/agent-templates/prometheus/MEMORY.md +9 -0
- package/agent-templates/prometheus/SOUL.md +21 -0
- package/agent-templates/prometheus/TOOLS.md +4 -0
- package/app/agents/ChannelManager.tsx +465 -0
- package/app/agents/ImageDownloadBanner.tsx +159 -0
- package/app/agents/PageClient.tsx +1285 -0
- package/app/agents/create/PageClient.tsx +437 -0
- package/app/agents/create/page.tsx +126 -0
- package/app/agents/loading.tsx +14 -0
- package/app/agents/page.tsx +5 -0
- package/app/api/agents/[id]/backup/route.ts +169 -0
- package/app/api/agents/[id]/channels/pairing/route.ts +69 -0
- package/app/api/agents/[id]/channels/route.ts +14 -0
- package/app/api/agents/[id]/channels/telegram/route.ts +50 -0
- package/app/api/agents/[id]/lifecycle/route.ts +94 -0
- package/app/api/agents/[id]/recreate/route.ts +199 -0
- package/app/api/agents/[id]/restart/route.ts +33 -0
- package/app/api/agents/[id]/restore/route.ts +81 -0
- package/app/api/agents/[id]/route.ts +280 -0
- package/app/api/agents/channels-summary/route.ts +39 -0
- package/app/api/agents/config-json/route.ts +58 -0
- package/app/api/agents/create/route.ts +488 -0
- package/app/api/agents/download-image/route.ts +32 -0
- package/app/api/agents/image-status/route.ts +126 -0
- package/app/api/agents/route.ts +213 -0
- package/app/api/agents/token/route.ts +94 -0
- package/app/api/agents-active/route.ts +190 -0
- package/app/api/agents-config/route.ts +629 -0
- package/app/api/alerts/smoke/route.ts +26 -0
- package/app/api/aliases/add/route.ts +28 -0
- package/app/api/aliases/remove/route.ts +27 -0
- package/app/api/assistant/route.ts +204 -0
- package/app/api/auth/check/route.ts +38 -0
- package/app/api/auth/login/route.ts +85 -0
- package/app/api/auth/logout/route.ts +7 -0
- package/app/api/auth/status/route.ts +33 -0
- package/app/api/config/env/route.ts +132 -0
- package/app/api/config/restart/route.ts +26 -0
- package/app/api/containers/route.ts +75 -0
- package/app/api/cost-override/route.ts +46 -0
- package/app/api/costs/route.ts +84 -0
- package/app/api/credentials/[id]/reveal/route.ts +21 -0
- package/app/api/credentials/[id]/route.ts +52 -0
- package/app/api/credentials/[id]/sync/route.ts +58 -0
- package/app/api/credentials/detect/route.ts +246 -0
- package/app/api/credentials/route.ts +33 -0
- package/app/api/crons/[id]/route.ts +69 -0
- package/app/api/crons/route.ts +12 -0
- package/app/api/crons/runs/route.ts +31 -0
- package/app/api/debug/route.ts +11 -0
- package/app/api/envcheck/route.ts +18 -0
- package/app/api/events/route.ts +24 -0
- package/app/api/gateway/agent/route.ts +154 -0
- package/app/api/gateway/provider/balance/route.ts +38 -0
- package/app/api/gateway/provider/keys.ts +38 -0
- package/app/api/gateway/provider/oauth/route.ts +304 -0
- package/app/api/gateway/provider/route.ts +209 -0
- package/app/api/gateway/route.ts +223 -0
- package/app/api/gateway/sync.ts +189 -0
- package/app/api/lineage/route.ts +54 -0
- package/app/api/memory-context/route.ts +15 -0
- package/app/api/metrics/route.ts +35 -0
- package/app/api/models/route.ts +77 -0
- package/app/api/plugins/route.js +37 -0
- package/app/api/provider/upstream.ts +142 -0
- package/app/api/provider/v1/chat/completions/route.ts +146 -0
- package/app/api/provider/v1/models/route.ts +167 -0
- package/app/api/provider-usage/route.ts +629 -0
- package/app/api/session/route.ts +39 -0
- package/app/api/sessions/[id]/route.ts +38 -0
- package/app/api/sessions/route.ts +23 -0
- package/app/api/setup/agent-image/route.ts +102 -0
- package/app/api/setup/password/route.ts +111 -0
- package/app/api/setup/restart/route.ts +76 -0
- package/app/api/skills/delete/route.js +105 -0
- package/app/api/skills/promote/route.js +129 -0
- package/app/api/skills/route.js +203 -0
- package/app/api/skills/save/route.js +142 -0
- package/app/api/stats/route.ts +34 -0
- package/app/api/stats-since/route.ts +20 -0
- package/app/api/stream/route.ts +70 -0
- package/app/api/system-health/route.ts +233 -0
- package/app/api/tool-calls/route.ts +24 -0
- package/app/api/tools-config/route.ts +163 -0
- package/app/api/update-check/route.ts +127 -0
- package/app/api/version/route.ts +25 -0
- package/app/api/wizard/complete/route.ts +32 -0
- package/app/api/wizard/reset/route.ts +28 -0
- package/app/api/wizard/status/route.ts +28 -0
- package/app/api/workspace/route.ts +392 -0
- package/app/api/workspace/stream/route.ts +136 -0
- package/app/components/AppShell.tsx +33 -0
- package/app/components/AuthGuard.tsx +91 -0
- package/app/components/CostBreakdown.tsx +37 -0
- package/app/components/DashboardHeader.tsx +73 -0
- package/app/components/DashboardLayout.tsx +76 -0
- package/app/components/DashboardToolbar.tsx +61 -0
- package/app/components/FloatingActions.tsx +33 -0
- package/app/components/Icons.tsx +78 -0
- package/app/components/LineageGraphPage.tsx +142 -0
- package/app/components/LiveFeed.tsx +48 -0
- package/app/components/MobileBottomNav.tsx +84 -0
- package/app/components/ModelPickerModal.tsx +94 -0
- package/app/components/PasswordInput.tsx +86 -0
- package/app/components/PulseChat.tsx +362 -0
- package/app/components/Rev4aLoader.tsx +48 -0
- package/app/components/SessionDrawer.tsx +263 -0
- package/app/components/SessionTopology.tsx +289 -0
- package/app/components/Sidebar.tsx +294 -0
- package/app/components/Skeleton.tsx +133 -0
- package/app/components/SystemCockpit.tsx +169 -0
- package/app/components/VersionBanner.tsx +105 -0
- package/app/components/WizardButton.tsx +30 -0
- package/app/components/ui/Badge.tsx +30 -0
- package/app/components/ui/Button.tsx +59 -0
- package/app/components/ui/ConfirmModal.tsx +46 -0
- package/app/components/ui/FilterBar.tsx +33 -0
- package/app/components/ui/Input.tsx +25 -0
- package/app/components/ui/ItemList.tsx +83 -0
- package/app/components/ui/LoadingSpinner.tsx +30 -0
- package/app/components/ui/Metric.tsx +22 -0
- package/app/components/ui/Modal.tsx +42 -0
- package/app/components/ui/Page.tsx +18 -0
- package/app/components/ui/Pill.tsx +6 -0
- package/app/components/ui/PropertyList.tsx +24 -0
- package/app/components/ui/Select.tsx +26 -0
- package/app/components/ui/StatusCard.tsx +32 -0
- package/app/components/ui/Surface.tsx +14 -0
- package/app/components/ui/Tabs.tsx +55 -0
- package/app/components/ui/TemplateOption.tsx +59 -0
- package/app/components/ui/Toast.tsx +44 -0
- package/app/components/ui/index.ts +19 -0
- package/app/components/ui/tokens.ts +18 -0
- package/app/config/PageClient.tsx +293 -0
- package/app/config/loading.tsx +10 -0
- package/app/config/page.tsx +7 -0
- package/app/containers/ContainersClient.tsx +129 -0
- package/app/containers/loading.tsx +21 -0
- package/app/containers/page.tsx +5 -0
- package/app/containers/terminal/[id]/TerminalClient.tsx +313 -0
- package/app/containers/terminal/[id]/page.tsx +6 -0
- package/app/credentials/PageClient.tsx +598 -0
- package/app/credentials/loading.tsx +10 -0
- package/app/credentials/page.tsx +7 -0
- package/app/crons/PageClient.tsx +538 -0
- package/app/crons/loading.tsx +10 -0
- package/app/crons/page.tsx +5 -0
- package/app/design-system.ts +57 -0
- package/app/gateway/PageClient.tsx +1048 -0
- package/app/gateway/loading.tsx +24 -0
- package/app/gateway/page.tsx +5 -0
- package/app/globals.css +1474 -0
- package/app/layout.tsx +45 -0
- package/app/lib/models-context.tsx +41 -0
- package/app/lineage/loading.tsx +10 -0
- package/app/lineage/page.tsx +13 -0
- package/app/loading.tsx +16 -0
- package/app/login/page.tsx +160 -0
- package/app/memory/MemoryContextPageClient.tsx +196 -0
- package/app/memory/loading.tsx +10 -0
- package/app/memory/page.tsx +8 -0
- package/app/page.tsx +31 -0
- package/app/plugins/PageClient.tsx +296 -0
- package/app/plugins/loading.tsx +10 -0
- package/app/plugins/page.tsx +5 -0
- package/app/setup/PageClient.tsx +227 -0
- package/app/setup/loading.tsx +21 -0
- package/app/setup/page.tsx +7 -0
- package/app/skills/PageClient.tsx +499 -0
- package/app/skills/loading.tsx +10 -0
- package/app/skills/page.tsx +5 -0
- package/app/tools/ToolsPageClient.tsx +308 -0
- package/app/tools/loading.tsx +10 -0
- package/app/tools/page.tsx +62 -0
- package/app/tools/tools-catalog.ts +141 -0
- package/app/wizard/PageClient.tsx +485 -0
- package/app/wizard/icons.tsx +89 -0
- package/app/wizard/loading.tsx +24 -0
- package/app/wizard/page.tsx +7 -0
- package/app/wizard/storage.ts +33 -0
- package/app/wizard/useWizard.ts +101 -0
- package/app/workspace/WorkspaceClient.tsx +633 -0
- package/app/workspace/loading.tsx +36 -0
- package/app/workspace/page.tsx +8 -0
- package/bin/docker-entrypoint.sh +21 -0
- package/bin/postinstall.js +20 -0
- package/bin/rev4a.js +696 -0
- package/daemon.js +783 -0
- package/docs/ARCHITECTURE.md +557 -0
- package/docs/CONTAINER-TERMINAL.md +280 -0
- package/docs/DESIGN-SYSTEM.md +99 -0
- package/docs/FRONTEND-ARCHITECTURE.md +119 -0
- package/docs/REV4A.md +504 -0
- package/docs/dev/API-REFERENCE.md +1835 -0
- package/docs/dev/DATABASE.md +224 -0
- package/docs/dev/GATEWAY.md +300 -0
- package/docs/dev/PROVIDERS.md +229 -0
- package/docs/dev/SESSION-MAINTENANCE-PLAN.md +607 -0
- package/docs/dev/WORKSPACE.md +199 -0
- package/docs/rag/DATA-FRESHNESS.md +109 -0
- package/docs/rag/GLOSSARY.md +125 -0
- package/docs/rag/REV4A-OVERVIEW.md +135 -0
- package/docs/rag/WHAT-I-CAN-ANSWER.md +124 -0
- package/lib/agent-setup.ts +130 -0
- package/lib/alerts.ts +115 -0
- package/lib/apiFetch.ts +7 -0
- package/lib/auth.ts +28 -0
- package/lib/billing.ts +100 -0
- package/lib/buildAgentImage.ts +346 -0
- package/lib/channelManager.ts +386 -0
- package/lib/container.ts +60 -0
- package/lib/credentials/db.ts +86 -0
- package/lib/credentials/delivery.ts +448 -0
- package/lib/credentials/detect.ts +149 -0
- package/lib/credentials/hash.ts +9 -0
- package/lib/credentials/providers.ts +110 -0
- package/lib/credentials/vault.ts +195 -0
- package/lib/db-bootstrap.d.ts +3 -0
- package/lib/db-bootstrap.mjs +144 -0
- package/lib/db.ts +25 -0
- package/lib/docker-utils.ts +60 -0
- package/lib/hooks/useDashboard.ts +264 -0
- package/lib/hooks/useRev4aTimezone.ts +28 -0
- package/lib/hooks/useScreenshot.ts +30 -0
- package/lib/memory-context.ts +385 -0
- package/lib/model-catalogue.ts +131 -0
- package/lib/model-pricing.ts +63 -0
- package/lib/openclaw-cron.ts +350 -0
- package/lib/patterns/ApiAdapter.ts +73 -0
- package/lib/patterns/ApiClient.ts +65 -0
- package/lib/patterns/EventBus.ts +113 -0
- package/lib/patterns/FilterStrategy.ts +79 -0
- package/lib/patterns/SessionFactory.ts +117 -0
- package/lib/patterns/sessionPresentation.ts +117 -0
- package/lib/provider-balance.ts +163 -0
- package/lib/requireAuth.tsx +38 -0
- package/lib/rev4a-auth.d.ts +3 -0
- package/lib/rev4a-auth.js +36 -0
- package/lib/rev4a-paths.ts +145 -0
- package/lib/timezone.ts +46 -0
- package/lib/types/index.ts +96 -0
- package/lib/utils/format.ts +82 -0
- package/lineage.js +56 -0
- package/model-pricing.json +482 -0
- package/models.config.json +844 -0
- package/next-env.d.ts +6 -0
- package/next.config.mjs +7 -0
- package/package.json +82 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon-64.png +0 -0
- package/public/favicon.svg +58 -0
- package/public/icon-192-maskable.png +0 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512-maskable.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/manifest.json +38 -0
- package/public/rev4a-logo.png +0 -0
- package/public/rev4a-logo.svg +58 -0
- package/rev4a-rules/AGENTS.md +37 -0
- package/scripts/add-auth.cjs +185 -0
- package/scripts/backup.sh +66 -0
- package/scripts/check-providers.mjs +143 -0
- package/scripts/docker/traefik/docker-compose.yml +32 -0
- package/scripts/docker/traefik/dynamic/.gitkeep +18 -0
- package/scripts/docker/traefik/traefik.yml +37 -0
- package/scripts/remove-auth-profile.js +56 -0
- package/scripts/restore.sh +82 -0
- package/terminal-ws-server.js +139 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
2
|
+
import { openDb } from '@/lib/db';
|
|
3
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
|
|
7
|
+
interface ProviderRow {
|
|
8
|
+
provider_key: string;
|
|
9
|
+
total_cost: number;
|
|
10
|
+
total_tokens: number;
|
|
11
|
+
session_count: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ProviderUsageEntry {
|
|
15
|
+
key: string;
|
|
16
|
+
label: string;
|
|
17
|
+
totalCost: number;
|
|
18
|
+
totalTokens: number;
|
|
19
|
+
sessionCount: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface QuotaMetric {
|
|
23
|
+
label: string;
|
|
24
|
+
used: number;
|
|
25
|
+
limit: number;
|
|
26
|
+
remaining: number;
|
|
27
|
+
unit: string;
|
|
28
|
+
period: string;
|
|
29
|
+
pct: number;
|
|
30
|
+
source?: string;
|
|
31
|
+
resetAt?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface OpenClawUsageWindow {
|
|
35
|
+
label: string;
|
|
36
|
+
usedPercent: number;
|
|
37
|
+
resetAt?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface OpenClawUsageProvider {
|
|
41
|
+
provider: string;
|
|
42
|
+
windows: OpenClawUsageWindow[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface OpenClawUsageSummary {
|
|
46
|
+
providers: OpenClawUsageProvider[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface DbSessionAggregate {
|
|
50
|
+
total_tokens: number;
|
|
51
|
+
session_count: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const MODEL_PATTERNS: Record<string, string[]> = {
|
|
55
|
+
'openai-codex': ['gpt-%', '%codex%', 'openai-codex/%'],
|
|
56
|
+
'github-copilot': ['gemini-%', 'github-copilot/%'],
|
|
57
|
+
'anthropic': ['claude-%', 'anthropic/%', 'claude-cli', 'claude-cli/%'],
|
|
58
|
+
'openrouter': ['openrouter/%'],
|
|
59
|
+
'groq': ['groq/%'],
|
|
60
|
+
'other': ['other/%'],
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
interface OpenAiUsageBucket {
|
|
64
|
+
n_context_tokens_total?: number;
|
|
65
|
+
n_generated_tokens_total?: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface RuntimeStatusQuotaMetric {
|
|
69
|
+
label: string;
|
|
70
|
+
used: number;
|
|
71
|
+
limit: number;
|
|
72
|
+
remaining: number;
|
|
73
|
+
unit: string;
|
|
74
|
+
period: string;
|
|
75
|
+
source: 'runtime-status';
|
|
76
|
+
pct: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type LoadProviderUsageSummary = (opts?: {
|
|
80
|
+
providers?: string[];
|
|
81
|
+
agentDir?: string;
|
|
82
|
+
timeoutMs?: number;
|
|
83
|
+
}) => Promise<OpenClawUsageSummary>;
|
|
84
|
+
|
|
85
|
+
type FsPromisesModule = typeof import('node:fs/promises');
|
|
86
|
+
|
|
87
|
+
const LABEL: Record<string, string> = {
|
|
88
|
+
anthropic: 'Anthropic',
|
|
89
|
+
'claude-cli': 'Claude CLI',
|
|
90
|
+
openrouter: 'OpenRouter',
|
|
91
|
+
'openai-codex': 'OpenAI Codex',
|
|
92
|
+
'github-copilot': 'GitHub Copilot',
|
|
93
|
+
groq: 'Groq',
|
|
94
|
+
other: 'Other',
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
function parseNumber(value: string | undefined, fallback: number): number {
|
|
98
|
+
if (!value) return fallback;
|
|
99
|
+
const parsed = Number.parseFloat(value);
|
|
100
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function loadFsPromises(): FsPromisesModule | null {
|
|
104
|
+
try {
|
|
105
|
+
return (0, eval)('require')('node:fs/promises') as FsPromisesModule;
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parseOptionalNumber(value: string | undefined): number | null {
|
|
112
|
+
if (!value) return null;
|
|
113
|
+
const parsed = Number.parseFloat(value);
|
|
114
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function roundMetric(value: number): number {
|
|
118
|
+
return Math.round(value * 100) / 100;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function clampPct(used: number, limit: number): number {
|
|
122
|
+
if (!(limit > 0)) return 0;
|
|
123
|
+
return Math.max(0, Math.min(100, roundMetric((used / limit) * 100)));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function buildQuotaMetric(input: {
|
|
127
|
+
label: string;
|
|
128
|
+
used: number;
|
|
129
|
+
limit: number;
|
|
130
|
+
unit: string;
|
|
131
|
+
period: string;
|
|
132
|
+
source?: string;
|
|
133
|
+
resetAt?: string;
|
|
134
|
+
}): QuotaMetric | null {
|
|
135
|
+
if (!(input.limit > 0)) return null;
|
|
136
|
+
const used = Math.max(0, roundMetric(input.used));
|
|
137
|
+
const limit = roundMetric(input.limit);
|
|
138
|
+
const remaining = Math.max(0, roundMetric(limit - used));
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
label: input.label,
|
|
142
|
+
used,
|
|
143
|
+
limit,
|
|
144
|
+
remaining,
|
|
145
|
+
unit: input.unit,
|
|
146
|
+
period: input.period,
|
|
147
|
+
pct: clampPct(used, limit),
|
|
148
|
+
source: input.source,
|
|
149
|
+
resetAt: input.resetAt,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function startOfDaySeconds(now = new Date()): number {
|
|
154
|
+
const value = new Date(now);
|
|
155
|
+
value.setHours(0, 0, 0, 0);
|
|
156
|
+
return Math.floor(value.getTime() / 1000);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function startOfWeekSeconds(now = new Date()): number {
|
|
160
|
+
const value = new Date(now);
|
|
161
|
+
const day = value.getDay();
|
|
162
|
+
const diff = day === 0 ? -6 : 1 - day;
|
|
163
|
+
value.setDate(value.getDate() + diff);
|
|
164
|
+
value.setHours(0, 0, 0, 0);
|
|
165
|
+
return Math.floor(value.getTime() / 1000);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function nextUtcMidnightIso(now = new Date()): string {
|
|
169
|
+
const value = new Date(now);
|
|
170
|
+
value.setUTCHours(24, 0, 0, 0);
|
|
171
|
+
return value.toISOString();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function nextWeekStartIso(now = new Date()): string {
|
|
175
|
+
const value = new Date(now);
|
|
176
|
+
const day = value.getDay();
|
|
177
|
+
const diff = day === 0 ? 1 : 8 - day;
|
|
178
|
+
value.setDate(value.getDate() + diff);
|
|
179
|
+
value.setHours(0, 0, 0, 0);
|
|
180
|
+
return value.toISOString();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function fetchOpenAiUsageTokens(apiKey: string, day: Date): Promise<number | null> {
|
|
184
|
+
try {
|
|
185
|
+
const date = day.toISOString().slice(0, 10);
|
|
186
|
+
const res = await fetch(`https://api.openai.com/v1/usage?date=${date}`, {
|
|
187
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
188
|
+
signal: AbortSignal.timeout(6000),
|
|
189
|
+
cache: 'no-store',
|
|
190
|
+
});
|
|
191
|
+
if (!res.ok) return null;
|
|
192
|
+
const json = await res.json() as { data?: OpenAiUsageBucket[] };
|
|
193
|
+
return (json.data ?? []).reduce(
|
|
194
|
+
(sum, bucket) => sum + (bucket.n_context_tokens_total ?? 0) + (bucket.n_generated_tokens_total ?? 0),
|
|
195
|
+
0,
|
|
196
|
+
);
|
|
197
|
+
} catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function getModelWhereClause(providerKey: string): string {
|
|
203
|
+
const patterns = MODEL_PATTERNS[providerKey];
|
|
204
|
+
if (!patterns) return `model LIKE '${providerKey}/%'`;
|
|
205
|
+
return patterns.map((p) => `model LIKE '${p}'`).join(' OR ');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let openClawUsageLoaderPromise: Promise<LoadProviderUsageSummary | null> | null = null;
|
|
209
|
+
const OPENCLAW_PROVIDER_USAGE_MODULE =
|
|
210
|
+
'/usr/local/lib/node_modules/openclaw/dist/provider-usage-Ccwl6xqF.js';
|
|
211
|
+
|
|
212
|
+
async function loadOpenClawUsageLoader(): Promise<LoadProviderUsageSummary | null> {
|
|
213
|
+
if (!openClawUsageLoaderPromise) {
|
|
214
|
+
openClawUsageLoaderPromise = import(
|
|
215
|
+
/* webpackIgnore: true */ OPENCLAW_PROVIDER_USAGE_MODULE
|
|
216
|
+
)
|
|
217
|
+
.then((mod: unknown) => {
|
|
218
|
+
const runtime = mod as { t?: LoadProviderUsageSummary };
|
|
219
|
+
return typeof runtime.t === 'function' ? runtime.t : null;
|
|
220
|
+
})
|
|
221
|
+
.catch(() => null);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return openClawUsageLoaderPromise;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function buildQuotaMetricFromRuntimeWindow(
|
|
228
|
+
provider: string,
|
|
229
|
+
window: OpenClawUsageWindow,
|
|
230
|
+
): QuotaMetric | null {
|
|
231
|
+
const usedPercent = Number.isFinite(window.usedPercent) ? Math.max(0, Math.min(100, window.usedPercent)) : 0;
|
|
232
|
+
const remainingPercent = Math.max(0, 100 - usedPercent);
|
|
233
|
+
return buildQuotaMetric({
|
|
234
|
+
label: provider === 'github-copilot' ? `${window.label} runtime quota` : `${window.label} runtime quota`,
|
|
235
|
+
used: roundMetric(usedPercent),
|
|
236
|
+
limit: 100,
|
|
237
|
+
unit: '%',
|
|
238
|
+
period: normalizeRuntimePeriod(window.label),
|
|
239
|
+
source: 'openclaw-runtime',
|
|
240
|
+
resetAt: typeof window.resetAt === 'number' ? new Date(window.resetAt).toISOString() : undefined,
|
|
241
|
+
}) ?? {
|
|
242
|
+
label: `${window.label} runtime quota`,
|
|
243
|
+
used: roundMetric(usedPercent),
|
|
244
|
+
limit: 100,
|
|
245
|
+
remaining: roundMetric(remainingPercent),
|
|
246
|
+
unit: '%',
|
|
247
|
+
period: normalizeRuntimePeriod(window.label),
|
|
248
|
+
pct: roundMetric(usedPercent),
|
|
249
|
+
source: 'openclaw-runtime',
|
|
250
|
+
resetAt: typeof window.resetAt === 'number' ? new Date(window.resetAt).toISOString() : undefined,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function normalizeRuntimePeriod(label: string): string {
|
|
255
|
+
const normalized = label.trim().toLowerCase();
|
|
256
|
+
if (normalized === 'week') return 'weekly';
|
|
257
|
+
if (normalized === 'day') return 'daily';
|
|
258
|
+
if (normalized.endsWith('h')) return normalized;
|
|
259
|
+
if (normalized === 'premium' || normalized === 'chat') return 'runtime';
|
|
260
|
+
return normalized || 'runtime';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function buildRuntimeQuotas(): Promise<Pick<Record<string, QuotaMetric[] | null>, 'anthropic' | 'claude-cli' | 'openai-codex' | 'github-copilot'>> {
|
|
264
|
+
const loader = await loadOpenClawUsageLoader();
|
|
265
|
+
if (!loader) {
|
|
266
|
+
return {
|
|
267
|
+
anthropic: null,
|
|
268
|
+
'claude-cli': null,
|
|
269
|
+
'openai-codex': null,
|
|
270
|
+
'github-copilot': null,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
const summary = await loader({
|
|
276
|
+
providers: ['anthropic', 'claude-cli', 'openai-codex', 'github-copilot'],
|
|
277
|
+
agentDir: '/data/.openclaw',
|
|
278
|
+
timeoutMs: 5000,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const byProvider = new Map(summary.providers.map((entry) => [entry.provider, entry]));
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
anthropic: (byProvider.get('anthropic')?.windows ?? [])
|
|
285
|
+
.map((window) => buildQuotaMetricFromRuntimeWindow('anthropic', window))
|
|
286
|
+
.filter((metric): metric is QuotaMetric => Boolean(metric)),
|
|
287
|
+
'claude-cli': (byProvider.get('claude-cli')?.windows ?? [])
|
|
288
|
+
.map((window) => buildQuotaMetricFromRuntimeWindow('claude-cli', window))
|
|
289
|
+
.filter((metric): metric is QuotaMetric => Boolean(metric)),
|
|
290
|
+
'openai-codex': (byProvider.get('openai-codex')?.windows ?? [])
|
|
291
|
+
.map((window) => buildQuotaMetricFromRuntimeWindow('openai-codex', window))
|
|
292
|
+
.filter((metric): metric is QuotaMetric => Boolean(metric)),
|
|
293
|
+
'github-copilot': (byProvider.get('github-copilot')?.windows ?? [])
|
|
294
|
+
.map((window) => buildQuotaMetricFromRuntimeWindow('github-copilot', window))
|
|
295
|
+
.filter((metric): metric is QuotaMetric => Boolean(metric)),
|
|
296
|
+
};
|
|
297
|
+
} catch {
|
|
298
|
+
return {
|
|
299
|
+
anthropic: null,
|
|
300
|
+
'claude-cli': null,
|
|
301
|
+
'openai-codex': null,
|
|
302
|
+
'github-copilot': null,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function fetchDbAggregate(
|
|
308
|
+
db: ReturnType<typeof openDb>,
|
|
309
|
+
providerKey: string,
|
|
310
|
+
startedAt: number,
|
|
311
|
+
): DbSessionAggregate {
|
|
312
|
+
const whereClause = getModelWhereClause(providerKey);
|
|
313
|
+
const row = db
|
|
314
|
+
.prepare(
|
|
315
|
+
`SELECT
|
|
316
|
+
COALESCE(SUM(tokens_in), 0) + COALESCE(SUM(tokens_out), 0) AS total_tokens,
|
|
317
|
+
COUNT(*) AS session_count
|
|
318
|
+
FROM sessions
|
|
319
|
+
WHERE (${whereClause})
|
|
320
|
+
AND started_at >= ?`,
|
|
321
|
+
)
|
|
322
|
+
.get(startedAt) as DbSessionAggregate | undefined;
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
total_tokens: row?.total_tokens ?? 0,
|
|
326
|
+
session_count: row?.session_count ?? 0,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const SESSION_STATUS_DIRS = [
|
|
331
|
+
'/data/.openclaw/agents/ops/sessions',
|
|
332
|
+
'/data/.openclaw/agents/main/sessions',
|
|
333
|
+
] as const;
|
|
334
|
+
const SESSION_STATUS_FILE_LIMIT = 30;
|
|
335
|
+
const SESSION_STATUS_LINE_LIMIT = 2_000;
|
|
336
|
+
|
|
337
|
+
function parseStatusDurationHours(value: string | undefined): number | null {
|
|
338
|
+
if (!value) return null;
|
|
339
|
+
const hourMatch = value.match(/(\d+(?:\.\d+)?)\s*h/i);
|
|
340
|
+
const minuteMatch = value.match(/(\d+(?:\.\d+)?)\s*m/i);
|
|
341
|
+
const hours = hourMatch ? Number.parseFloat(hourMatch[1]) : 0;
|
|
342
|
+
const minutes = minuteMatch ? Number.parseFloat(minuteMatch[1]) : 0;
|
|
343
|
+
const total = hours + (minutes / 60);
|
|
344
|
+
return total > 0 ? roundMetric(total) : null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function buildRuntimeStatusQuotaMetric(input: {
|
|
348
|
+
label: string;
|
|
349
|
+
period: 'daily' | 'weekly';
|
|
350
|
+
limit: number;
|
|
351
|
+
remainingPct: number;
|
|
352
|
+
remainingLabel?: string;
|
|
353
|
+
unit: string;
|
|
354
|
+
}): RuntimeStatusQuotaMetric {
|
|
355
|
+
const remainingPct = Math.max(0, Math.min(100, input.remainingPct));
|
|
356
|
+
const usedPct = roundMetric(100 - remainingPct);
|
|
357
|
+
const used = roundMetric(input.limit * (usedPct / 100));
|
|
358
|
+
const remaining = input.unit === 'hours' && input.remainingLabel
|
|
359
|
+
? parseStatusDurationHours(input.remainingLabel) ?? roundMetric(input.limit * (remainingPct / 100))
|
|
360
|
+
: roundMetric(input.limit * (remainingPct / 100));
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
label: input.label,
|
|
364
|
+
used,
|
|
365
|
+
limit: roundMetric(input.limit),
|
|
366
|
+
remaining,
|
|
367
|
+
unit: input.unit,
|
|
368
|
+
period: input.period,
|
|
369
|
+
source: 'runtime-status',
|
|
370
|
+
pct: usedPct,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function parseCodexRuntimeStatus(statusText: string): QuotaMetric[] | null {
|
|
375
|
+
const hasCodexModel = /Model:\s*openai-codex\//i.test(statusText) || /Runtime:\s*OpenAI Codex/i.test(statusText);
|
|
376
|
+
const usageLine = statusText
|
|
377
|
+
.split('\n')
|
|
378
|
+
.map((line) => line.trim())
|
|
379
|
+
.find((line) => /Usage:/i.test(line));
|
|
380
|
+
|
|
381
|
+
if (!hasCodexModel || !usageLine) return null;
|
|
382
|
+
|
|
383
|
+
const usageMatch = usageLine.match(/Usage:\s*(\d+(?:\.\d+)?)h\s+(\d+)%\s+left(?:\s+⏱\s*([^·]+?))?(?:\s+·\s+Week\s+(\d+)%\s+left(?:\s+⏱\s*(.+))?)?$/i);
|
|
384
|
+
if (!usageMatch) return null;
|
|
385
|
+
|
|
386
|
+
const [, dailyLimitHoursRaw, dailyRemainingPctRaw, dailyRemainingLabel, weeklyRemainingPctRaw, weeklyRemainingLabel] = usageMatch;
|
|
387
|
+
const dailyLimitHours = Number.parseFloat(dailyLimitHoursRaw);
|
|
388
|
+
const dailyRemainingPct = Number.parseFloat(dailyRemainingPctRaw);
|
|
389
|
+
if (!Number.isFinite(dailyLimitHours) || !Number.isFinite(dailyRemainingPct)) return null;
|
|
390
|
+
|
|
391
|
+
const metrics: QuotaMetric[] = [buildRuntimeStatusQuotaMetric({
|
|
392
|
+
label: 'Daily runtime quota',
|
|
393
|
+
period: 'daily',
|
|
394
|
+
limit: dailyLimitHours,
|
|
395
|
+
remainingPct: dailyRemainingPct,
|
|
396
|
+
remainingLabel: dailyRemainingLabel?.trim(),
|
|
397
|
+
unit: 'hours',
|
|
398
|
+
})];
|
|
399
|
+
|
|
400
|
+
if (weeklyRemainingPctRaw) {
|
|
401
|
+
const weeklyRemainingPct = Number.parseFloat(weeklyRemainingPctRaw);
|
|
402
|
+
if (Number.isFinite(weeklyRemainingPct)) {
|
|
403
|
+
metrics.push(buildRuntimeStatusQuotaMetric({
|
|
404
|
+
label: 'Weekly quota',
|
|
405
|
+
period: 'weekly',
|
|
406
|
+
limit: 100,
|
|
407
|
+
remainingPct: weeklyRemainingPct,
|
|
408
|
+
remainingLabel: weeklyRemainingLabel?.trim(),
|
|
409
|
+
unit: 'percent',
|
|
410
|
+
}));
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return metrics;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function findLatestCodexRuntimeStatus(): Promise<QuotaMetric[] | null> {
|
|
418
|
+
try {
|
|
419
|
+
const runtimeFs = loadFsPromises();
|
|
420
|
+
if (!runtimeFs) return null;
|
|
421
|
+
|
|
422
|
+
const files = (
|
|
423
|
+
await Promise.all(
|
|
424
|
+
SESSION_STATUS_DIRS.map(async (dir) => {
|
|
425
|
+
const entries = await runtimeFs.readdir(dir, { withFileTypes: true });
|
|
426
|
+
const stats = await Promise.all(entries
|
|
427
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.jsonl'))
|
|
428
|
+
.map(async (entry) => {
|
|
429
|
+
const filePath = `${dir}/${entry.name}`;
|
|
430
|
+
const stat = await runtimeFs.stat(filePath);
|
|
431
|
+
return { filePath, mtimeMs: stat.mtimeMs };
|
|
432
|
+
}));
|
|
433
|
+
return stats;
|
|
434
|
+
}),
|
|
435
|
+
)
|
|
436
|
+
)
|
|
437
|
+
.flat()
|
|
438
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs)
|
|
439
|
+
.slice(0, SESSION_STATUS_FILE_LIMIT);
|
|
440
|
+
|
|
441
|
+
for (const file of files) {
|
|
442
|
+
const content = await runtimeFs.readFile(file.filePath, 'utf8');
|
|
443
|
+
const lines = content.split('\n');
|
|
444
|
+
for (let index = lines.length - 1, seen = 0; index >= 0 && seen < SESSION_STATUS_LINE_LIMIT; index -= 1, seen += 1) {
|
|
445
|
+
const line = lines[index]?.trim();
|
|
446
|
+
if (!line) continue;
|
|
447
|
+
try {
|
|
448
|
+
const parsed = JSON.parse(line) as {
|
|
449
|
+
message?: {
|
|
450
|
+
role?: string;
|
|
451
|
+
toolName?: string;
|
|
452
|
+
details?: { statusText?: unknown };
|
|
453
|
+
};
|
|
454
|
+
};
|
|
455
|
+
const statusText = parsed.message?.details?.statusText;
|
|
456
|
+
if (
|
|
457
|
+
parsed.message?.role === 'toolResult'
|
|
458
|
+
&& parsed.message?.toolName === 'session_status'
|
|
459
|
+
&& typeof statusText === 'string'
|
|
460
|
+
) {
|
|
461
|
+
const metrics = parseCodexRuntimeStatus(statusText);
|
|
462
|
+
if (metrics?.length) return metrics;
|
|
463
|
+
}
|
|
464
|
+
} catch {
|
|
465
|
+
// Ignore malformed lines.
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
} catch {
|
|
470
|
+
// Ignore filesystem/runtime-status failures and keep legacy fallback.
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function buildCodexQuotas(db: ReturnType<typeof openDb>): Promise<QuotaMetric[] | null> {
|
|
477
|
+
const runtimeStatusQuotas = await findLatestCodexRuntimeStatus();
|
|
478
|
+
if (runtimeStatusQuotas?.length) return runtimeStatusQuotas;
|
|
479
|
+
|
|
480
|
+
const metrics: QuotaMetric[] = [];
|
|
481
|
+
const now = new Date();
|
|
482
|
+
const tokensPerHour = parseNumber(process.env.OPENAI_CODEX_TOKENS_PER_HOUR, 1_000_000);
|
|
483
|
+
const dailyHourLimit = parseNumber(process.env.OPENAI_CODEX_DAILY_HOUR_LIMIT, 5);
|
|
484
|
+
const weeklyHourLimit = parseOptionalNumber(process.env.OPENAI_CODEX_WEEKLY_HOUR_LIMIT);
|
|
485
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
486
|
+
|
|
487
|
+
const dailyDbAggregate = fetchDbAggregate(db, 'openai-codex', startOfDaySeconds(now));
|
|
488
|
+
const dailyApiTokens = apiKey ? await fetchOpenAiUsageTokens(apiKey, now) : null;
|
|
489
|
+
const dailyTokens = dailyApiTokens ?? dailyDbAggregate.total_tokens;
|
|
490
|
+
const dailyMetric = buildQuotaMetric({
|
|
491
|
+
label: 'Daily cloud quota',
|
|
492
|
+
used: dailyTokens / tokensPerHour,
|
|
493
|
+
limit: dailyHourLimit,
|
|
494
|
+
unit: 'hours',
|
|
495
|
+
period: 'daily',
|
|
496
|
+
source: dailyApiTokens !== null ? 'api' : 'estimated',
|
|
497
|
+
resetAt: nextUtcMidnightIso(now),
|
|
498
|
+
});
|
|
499
|
+
if (dailyMetric) metrics.push(dailyMetric);
|
|
500
|
+
|
|
501
|
+
if (weeklyHourLimit !== null) {
|
|
502
|
+
const weeklyDbAggregate = fetchDbAggregate(db, 'openai-codex', startOfWeekSeconds(now));
|
|
503
|
+
const weeklyMetric = buildQuotaMetric({
|
|
504
|
+
label: 'Weekly cloud quota',
|
|
505
|
+
used: weeklyDbAggregate.total_tokens / tokensPerHour,
|
|
506
|
+
limit: weeklyHourLimit,
|
|
507
|
+
unit: 'hours',
|
|
508
|
+
period: 'weekly',
|
|
509
|
+
source: 'db-proxy',
|
|
510
|
+
resetAt: nextWeekStartIso(now),
|
|
511
|
+
});
|
|
512
|
+
if (weeklyMetric) metrics.push(weeklyMetric);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return metrics.length > 0 ? metrics : null;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function buildCopilotQuotas(db: ReturnType<typeof openDb>): QuotaMetric[] | null {
|
|
519
|
+
const monthlyLimit =
|
|
520
|
+
parseOptionalNumber(process.env.GITHUB_COPILOT_AI_CREDIT_LIMIT) ??
|
|
521
|
+
parseOptionalNumber(process.env.GITHUB_COPILOT_MONTHLY_LIMIT);
|
|
522
|
+
if (monthlyLimit === null) return null;
|
|
523
|
+
|
|
524
|
+
const now = new Date();
|
|
525
|
+
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
526
|
+
monthStart.setHours(0, 0, 0, 0);
|
|
527
|
+
const aggregate = fetchDbAggregate(db, 'github-copilot', Math.floor(monthStart.getTime() / 1000));
|
|
528
|
+
const metric = buildQuotaMetric({
|
|
529
|
+
label: 'AI credits',
|
|
530
|
+
used: aggregate.session_count,
|
|
531
|
+
limit: monthlyLimit,
|
|
532
|
+
unit: 'credits',
|
|
533
|
+
period: 'monthly',
|
|
534
|
+
source: 'db-proxy',
|
|
535
|
+
resetAt: new Date(now.getFullYear(), now.getMonth() + 1, 1).toISOString(),
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
return metric ? [metric] : null;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
542
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
543
|
+
let providers: ProviderUsageEntry[] = [];
|
|
544
|
+
const quotas: Record<string, QuotaMetric[] | null> = {
|
|
545
|
+
anthropic: null,
|
|
546
|
+
'claude-cli': null,
|
|
547
|
+
openrouter: null,
|
|
548
|
+
'openai-codex': null,
|
|
549
|
+
'github-copilot': null,
|
|
550
|
+
groq: null,
|
|
551
|
+
other: null,
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
let openrouterLive: { usage: number; limit: number; limitRemaining: number } | null = null;
|
|
555
|
+
const orKey = process.env.OPENROUTER_API_KEY;
|
|
556
|
+
|
|
557
|
+
try {
|
|
558
|
+
const db = openDb();
|
|
559
|
+
try {
|
|
560
|
+
const rows = db
|
|
561
|
+
.prepare(
|
|
562
|
+
`SELECT
|
|
563
|
+
CASE
|
|
564
|
+
WHEN model = 'claude-cli' OR model LIKE 'claude-cli/%' THEN 'claude-cli'
|
|
565
|
+
WHEN model LIKE 'anthropic/%' THEN 'anthropic'
|
|
566
|
+
WHEN model LIKE 'claude-%' THEN 'anthropic'
|
|
567
|
+
WHEN model LIKE 'openrouter/%' THEN 'openrouter'
|
|
568
|
+
WHEN model LIKE 'openai-codex/%' THEN 'openai-codex'
|
|
569
|
+
WHEN model LIKE 'github-copilot/%' THEN 'github-copilot'
|
|
570
|
+
WHEN model LIKE 'groq/%' THEN 'groq'
|
|
571
|
+
WHEN model LIKE 'gpt-%' OR model LIKE '%codex%' THEN 'openai-codex'
|
|
572
|
+
WHEN model LIKE 'gemini-%' THEN 'github-copilot'
|
|
573
|
+
ELSE 'other'
|
|
574
|
+
END AS provider_key,
|
|
575
|
+
COALESCE(SUM(CASE WHEN model LIKE 'openrouter/%' THEN cost_usd ELSE 0 END), 0) AS total_cost,
|
|
576
|
+
COALESCE(SUM(tokens_in), 0) + COALESCE(SUM(tokens_out), 0) AS total_tokens,
|
|
577
|
+
COUNT(*) AS session_count
|
|
578
|
+
FROM sessions
|
|
579
|
+
WHERE model IS NOT NULL
|
|
580
|
+
GROUP BY provider_key
|
|
581
|
+
ORDER BY total_cost DESC`,
|
|
582
|
+
)
|
|
583
|
+
.all() as ProviderRow[];
|
|
584
|
+
|
|
585
|
+
providers = rows.map((row) => ({
|
|
586
|
+
key: row.provider_key,
|
|
587
|
+
label: LABEL[row.provider_key] ?? row.provider_key,
|
|
588
|
+
totalCost: row.total_cost,
|
|
589
|
+
totalTokens: row.total_tokens,
|
|
590
|
+
sessionCount: row.session_count,
|
|
591
|
+
}));
|
|
592
|
+
|
|
593
|
+
const runtimeQuotas = await buildRuntimeQuotas();
|
|
594
|
+
quotas.anthropic = runtimeQuotas.anthropic;
|
|
595
|
+
quotas['claude-cli'] = runtimeQuotas['claude-cli'];
|
|
596
|
+
// Prefer runtime data (OpenClaw gateway) over DB estimates
|
|
597
|
+
{
|
|
598
|
+
const codexQuotas = await buildCodexQuotas(db);
|
|
599
|
+
const runtimeCodex = runtimeQuotas['openai-codex'];
|
|
600
|
+
quotas['openai-codex'] = (runtimeCodex && runtimeCodex.length > 0) ? runtimeCodex : codexQuotas;
|
|
601
|
+
}
|
|
602
|
+
quotas['github-copilot'] = runtimeQuotas['github-copilot'];
|
|
603
|
+
} finally {
|
|
604
|
+
db.close();
|
|
605
|
+
}
|
|
606
|
+
} catch {
|
|
607
|
+
// Leave providers/quotas empty when DB is unavailable.
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (orKey) {
|
|
611
|
+
const orResult = await fetch('https://openrouter.ai/api/v1/auth/key', {
|
|
612
|
+
headers: { Authorization: `Bearer ${orKey}` },
|
|
613
|
+
signal: AbortSignal.timeout(5000),
|
|
614
|
+
cache: 'no-store',
|
|
615
|
+
})
|
|
616
|
+
.then((response) => (response.ok ? response.json() : null))
|
|
617
|
+
.catch(() => null) as { data?: { usage?: number; limit?: number; limit_remaining?: number } } | null;
|
|
618
|
+
|
|
619
|
+
if (orResult?.data) {
|
|
620
|
+
openrouterLive = {
|
|
621
|
+
usage: orResult.data.usage ?? 0,
|
|
622
|
+
limit: orResult.data.limit ?? 0,
|
|
623
|
+
limitRemaining: orResult.data.limit_remaining ?? 0,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return NextResponse.json({ providers, openrouterLive, quotas });
|
|
629
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
2
|
+
import { openDb } from '@/lib/db';
|
|
3
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
|
|
7
|
+
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
8
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
9
|
+
const sessionId = new URL(request.url).searchParams.get('id');
|
|
10
|
+
if (!sessionId) return NextResponse.json({ error: 'Missing id' }, { status: 400 });
|
|
11
|
+
try {
|
|
12
|
+
const db = openDb();
|
|
13
|
+
const session = db
|
|
14
|
+
.prepare(`SELECT s.*, l.label AS lineage_label, l.agent_name AS lineage_agent_name
|
|
15
|
+
FROM sessions s
|
|
16
|
+
LEFT JOIN lineage l ON s.session_id = l.child_id
|
|
17
|
+
WHERE s.session_id = ?`)
|
|
18
|
+
.get(sessionId);
|
|
19
|
+
if (!session) {
|
|
20
|
+
db.close();
|
|
21
|
+
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
22
|
+
}
|
|
23
|
+
const events = db
|
|
24
|
+
.prepare('SELECT * FROM events WHERE session_id = ? ORDER BY ts DESC LIMIT 20')
|
|
25
|
+
.all(sessionId);
|
|
26
|
+
const children = db
|
|
27
|
+
.prepare(
|
|
28
|
+
`SELECT s.*, l.label AS lineage_label, l.agent_name AS lineage_agent_name
|
|
29
|
+
FROM sessions s
|
|
30
|
+
JOIN lineage l ON s.session_id = l.child_id
|
|
31
|
+
WHERE l.parent_id = ? ORDER BY s.started_at DESC LIMIT 10`,
|
|
32
|
+
)
|
|
33
|
+
.all(sessionId);
|
|
34
|
+
db.close();
|
|
35
|
+
return NextResponse.json({ session, events, children });
|
|
36
|
+
} catch (e: unknown) {
|
|
37
|
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
2
|
+
import { openDb } from '@/lib/db';
|
|
3
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
4
|
+
|
|
5
|
+
export async function GET(
|
|
6
|
+
request: NextRequest,
|
|
7
|
+
{ params }: { params: Promise<{ id: string }> },
|
|
8
|
+
): Promise<NextResponse> {
|
|
9
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
10
|
+
const { id } = await params;
|
|
11
|
+
const sessionId = decodeURIComponent(id);
|
|
12
|
+
const db = openDb();
|
|
13
|
+
try {
|
|
14
|
+
const session = db
|
|
15
|
+
.prepare(`SELECT s.*, l.label AS lineage_label, l.agent_name AS lineage_agent_name
|
|
16
|
+
FROM sessions s
|
|
17
|
+
LEFT JOIN lineage l ON s.session_id = l.child_id
|
|
18
|
+
WHERE s.session_id = ?`)
|
|
19
|
+
.get(sessionId);
|
|
20
|
+
if (!session) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
21
|
+
const events = db
|
|
22
|
+
.prepare('SELECT * FROM events WHERE session_id = ? ORDER BY ts DESC LIMIT 20')
|
|
23
|
+
.all(sessionId);
|
|
24
|
+
const children = db
|
|
25
|
+
.prepare(
|
|
26
|
+
`SELECT s.*, l.label AS lineage_label, l.agent_name AS lineage_agent_name
|
|
27
|
+
FROM sessions s
|
|
28
|
+
JOIN lineage l ON s.session_id = l.child_id
|
|
29
|
+
WHERE l.parent_id = ? ORDER BY s.started_at DESC LIMIT 10`,
|
|
30
|
+
)
|
|
31
|
+
.all(sessionId);
|
|
32
|
+
return NextResponse.json({ session, events, children });
|
|
33
|
+
} catch (e: unknown) {
|
|
34
|
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
35
|
+
} finally {
|
|
36
|
+
db.close();
|
|
37
|
+
}
|
|
38
|
+
}
|