@littlebearapps/platform-admin-sdk 1.4.2 → 2.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/dist/templates.d.ts +1 -1
- package/dist/templates.js +232 -2
- package/package.json +1 -1
- package/templates/full/config/audit-targets.yaml +72 -0
- package/templates/full/dashboard/src/components/notifications/NotificationBell.tsx +30 -0
- package/templates/full/dashboard/src/components/notifications/NotificationList.tsx +116 -0
- package/templates/full/dashboard/src/components/notifications/index.ts +2 -0
- package/templates/full/dashboard/src/components/patterns/ActivePatterns.tsx +62 -0
- package/templates/full/dashboard/src/components/patterns/PatternStats.tsx +60 -0
- package/templates/full/dashboard/src/components/patterns/PatternTabs.tsx +116 -0
- package/templates/full/dashboard/src/components/patterns/SuggestionsQueue.tsx +115 -0
- package/templates/full/dashboard/src/components/patterns/SystemPatterns.tsx +52 -0
- package/templates/full/dashboard/src/components/patterns/index.ts +5 -0
- package/templates/full/dashboard/src/components/reports/GapDetectionReport.tsx +69 -0
- package/templates/full/dashboard/src/components/reports/SdkAuditReport.tsx +72 -0
- package/templates/full/dashboard/src/components/reports/index.ts +2 -0
- package/templates/full/dashboard/src/components/search/SearchModal.tsx +108 -0
- package/templates/full/dashboard/src/pages/api/notifications/[id]/read.ts +37 -0
- package/templates/full/dashboard/src/pages/api/notifications/index.ts +47 -0
- package/templates/full/dashboard/src/pages/api/notifications/read-all.ts +28 -0
- package/templates/full/dashboard/src/pages/api/notifications/unread-count.ts +31 -0
- package/templates/full/dashboard/src/pages/api/patterns/approve.ts +55 -0
- package/templates/full/dashboard/src/pages/api/patterns/cache-refresh.ts +38 -0
- package/templates/full/dashboard/src/pages/api/patterns/discover.ts +36 -0
- package/templates/full/dashboard/src/pages/api/patterns/index.ts +36 -0
- package/templates/full/dashboard/src/pages/api/patterns/ready-for-review.ts +39 -0
- package/templates/full/dashboard/src/pages/api/patterns/reject.ts +54 -0
- package/templates/full/dashboard/src/pages/api/patterns/stats.ts +39 -0
- package/templates/full/dashboard/src/pages/api/patterns/suggestions.ts +43 -0
- package/templates/full/dashboard/src/pages/api/reports/audit.ts +45 -0
- package/templates/full/dashboard/src/pages/api/reports/usage.ts +52 -0
- package/templates/full/dashboard/src/pages/api/search/index.ts +74 -0
- package/templates/full/dashboard/src/pages/api/search/reindex.ts +28 -0
- package/templates/full/dashboard/src/pages/api/search/stats.ts +27 -0
- package/templates/full/dashboard/src/pages/api/settings/index.ts +37 -0
- package/templates/full/dashboard/src/pages/api/settings/update.ts +41 -0
- package/templates/full/dashboard/src/pages/api/topology/index.ts +56 -0
- package/templates/full/dashboard/src/pages/notifications.astro +11 -0
- package/templates/full/migrations/008_auditor.sql +99 -0
- package/templates/full/migrations/010_pricing_versions.sql +110 -0
- package/templates/full/migrations/011_multi_account.sql +51 -0
- package/templates/full/scripts/ops/set-kv-pricing.ts +182 -0
- package/templates/full/scripts/ops/universal-backfill.ts +147 -0
- package/templates/full/workers/lib/ai-judge-schema.ts +181 -0
- package/templates/full/workers/lib/auditor/comprehensive-report.ts +407 -0
- package/templates/full/workers/lib/auditor/feature-coverage.ts +348 -0
- package/templates/full/workers/lib/auditor/index.ts +9 -0
- package/templates/full/workers/lib/auditor/types.ts +167 -0
- package/templates/full/workers/platform-auditor.ts +1071 -0
- package/templates/full/wrangler.auditor.jsonc.hbs +75 -0
- package/templates/shared/.github/workflows/contract-check.yml.hbs +42 -0
- package/templates/shared/.github/workflows/dashboard-deploy.yml.hbs +39 -0
- package/templates/shared/.github/workflows/platform-check.yml.hbs +28 -0
- package/templates/shared/.github/workflows/security.yml +33 -0
- package/templates/shared/config/observability.yaml.hbs +276 -0
- package/templates/shared/contracts/schemas/envelope.v1.schema.json +64 -0
- package/templates/shared/contracts/schemas/error_report.v1.schema.json +65 -0
- package/templates/shared/contracts/types/telemetry-envelope.types.ts +139 -0
- package/templates/shared/dashboard/astro.config.mjs +21 -0
- package/templates/shared/dashboard/package.json.hbs +29 -0
- package/templates/shared/dashboard/src/components/Header.astro +29 -0
- package/templates/shared/dashboard/src/components/Nav.astro.hbs +59 -0
- package/templates/shared/dashboard/src/components/infrastructure/AlertHistory.tsx +57 -0
- package/templates/shared/dashboard/src/components/infrastructure/InfrastructureStats.tsx +73 -0
- package/templates/shared/dashboard/src/components/infrastructure/ServiceRegistry.tsx +55 -0
- package/templates/shared/dashboard/src/components/infrastructure/UptimeStatus.tsx +56 -0
- package/templates/shared/dashboard/src/components/infrastructure/index.ts +4 -0
- package/templates/shared/dashboard/src/components/overview/ActivityFeed.tsx +134 -0
- package/templates/shared/dashboard/src/components/overview/CostQuadrant.tsx +131 -0
- package/templates/shared/dashboard/src/components/overview/ErrorsQuadrant.tsx +113 -0
- package/templates/shared/dashboard/src/components/overview/HealthQuadrant.tsx +87 -0
- package/templates/shared/dashboard/src/components/overview/MissionControl.tsx +139 -0
- package/templates/shared/dashboard/src/components/resources/AllowanceStatus.tsx +44 -0
- package/templates/shared/dashboard/src/components/resources/CostCentreOverview.tsx +42 -0
- package/templates/shared/dashboard/src/components/resources/ResourceTabs.tsx +69 -0
- package/templates/shared/dashboard/src/components/resources/index.ts +3 -0
- package/templates/shared/dashboard/src/components/settings/SettingsCard.tsx +21 -0
- package/templates/shared/dashboard/src/components/settings/index.ts +1 -0
- package/templates/shared/dashboard/src/components/ui/AlertBanner.tsx +39 -0
- package/templates/shared/dashboard/src/components/ui/Breadcrumbs.tsx +27 -0
- package/templates/shared/dashboard/src/components/ui/EmptyState.tsx +26 -0
- package/templates/shared/dashboard/src/components/ui/ErrorBoundary.tsx +42 -0
- package/templates/shared/dashboard/src/components/ui/LoadingSkeleton.tsx +18 -0
- package/templates/shared/dashboard/src/components/ui/PageShell.tsx +26 -0
- package/templates/shared/dashboard/src/components/ui/Sparkline.tsx +127 -0
- package/templates/shared/dashboard/src/components/ui/StatusDot.tsx +21 -0
- package/templates/shared/dashboard/src/components/ui/Toast.tsx +44 -0
- package/templates/shared/dashboard/src/components/ui/index.ts +9 -0
- package/templates/shared/dashboard/src/components/usage/AnomaliesWidget.tsx +68 -0
- package/templates/shared/dashboard/src/components/usage/HourlyUsageChart.tsx +55 -0
- package/templates/shared/dashboard/src/components/usage/PlanAllowanceDashboard.tsx +67 -0
- package/templates/shared/dashboard/src/components/usage/ProjectCostBreakdown.tsx +55 -0
- package/templates/shared/dashboard/src/components/usage/index.ts +4 -0
- package/templates/shared/dashboard/src/env.d.ts.hbs +34 -0
- package/templates/shared/dashboard/src/layouts/DashboardLayout.astro +37 -0
- package/templates/shared/dashboard/src/lib/cloudflare/costs.ts +21 -0
- package/templates/shared/dashboard/src/lib/fetch.ts +29 -0
- package/templates/shared/dashboard/src/lib/types.ts +72 -0
- package/templates/shared/dashboard/src/middleware/auth.ts +100 -0
- package/templates/shared/dashboard/src/middleware/index.ts +1 -0
- package/templates/shared/dashboard/src/pages/api/costs/overview.ts +65 -0
- package/templates/shared/dashboard/src/pages/api/costs/providers.ts +47 -0
- package/templates/shared/dashboard/src/pages/api/infrastructure/services.ts +55 -0
- package/templates/shared/dashboard/src/pages/api/infrastructure/stats.ts +99 -0
- package/templates/shared/dashboard/src/pages/api/overview/summary.ts +311 -0
- package/templates/shared/dashboard/src/pages/api/usage/allowances.ts +56 -0
- package/templates/shared/dashboard/src/pages/api/usage/anomalies.ts +45 -0
- package/templates/shared/dashboard/src/pages/api/usage/billing.ts +53 -0
- package/templates/shared/dashboard/src/pages/api/usage/circuit-breakers.ts +44 -0
- package/templates/shared/dashboard/src/pages/api/usage/granular.ts +50 -0
- package/templates/shared/dashboard/src/pages/api/usage/hourly.ts +45 -0
- package/templates/shared/dashboard/src/pages/api/usage/projects.ts +51 -0
- package/templates/shared/dashboard/src/pages/api/usage/status.ts +42 -0
- package/templates/shared/dashboard/src/pages/api/user/identity.ts +11 -0
- package/templates/shared/dashboard/src/pages/dashboard.astro +11 -0
- package/templates/shared/dashboard/src/pages/index.astro +3 -0
- package/templates/shared/dashboard/src/pages/resources.astro +11 -0
- package/templates/shared/dashboard/src/pages/settings/index.astro +28 -0
- package/templates/shared/dashboard/src/pages/settings/notifications.astro +34 -0
- package/templates/shared/dashboard/src/pages/settings/thresholds.astro +39 -0
- package/templates/shared/dashboard/src/pages/settings/usage.astro +28 -0
- package/templates/shared/dashboard/src/styles/global.css +29 -0
- package/templates/shared/dashboard/tailwind.config.mjs +9 -0
- package/templates/shared/dashboard/tsconfig.json +9 -0
- package/templates/shared/dashboard/wrangler.json.hbs +47 -0
- package/templates/shared/docs/architecture.md +89 -0
- package/templates/shared/docs/post-deploy-runbook.md +126 -0
- package/templates/shared/docs/troubleshooting.md +91 -0
- package/templates/shared/package.json.hbs +17 -1
- package/templates/shared/scripts/ops/backfill-cloudflare-daily.ts +145 -0
- package/templates/shared/scripts/ops/backfill-cloudflare-hourly.ts +473 -0
- package/templates/shared/scripts/ops/backfill-monthly-rollups.ts +125 -0
- package/templates/shared/scripts/ops/discover-graphql-datasets.ts +482 -0
- package/templates/shared/scripts/ops/reset-budget-state.ts +279 -0
- package/templates/shared/scripts/ops/validate-controls.js +141 -0
- package/templates/shared/scripts/ops/validate-pipeline.ts +237 -0
- package/templates/shared/scripts/ops/verify-account-completeness.ts +236 -0
- package/templates/shared/scripts/validate-schemas.js +61 -0
- package/templates/shared/tests/contract/validate-schemas.test.ts +130 -0
- package/templates/shared/tests/fixtures/telemetry-envelope-invalid.json +9 -0
- package/templates/shared/tests/fixtures/telemetry-envelope-valid.json +27 -0
- package/templates/shared/tests/helpers/mock-d1.ts +61 -0
- package/templates/shared/tests/helpers/mock-kv.ts +37 -0
- package/templates/shared/tests/unit/workers/batch-persistence.test.ts +133 -0
- package/templates/shared/tests/unit/workers/budget-enforcement.test.ts +214 -0
- package/templates/shared/vitest.config.ts +18 -0
- package/templates/shared/workers/lib/usage/collectors/anthropic.ts +114 -0
- package/templates/shared/workers/lib/usage/collectors/apify.ts +96 -0
- package/templates/shared/workers/lib/usage/collectors/custom-http.ts +151 -0
- package/templates/shared/workers/lib/usage/collectors/deepseek.ts +92 -0
- package/templates/shared/workers/lib/usage/collectors/gemini.ts +263 -0
- package/templates/shared/workers/lib/usage/collectors/github.ts +362 -0
- package/templates/shared/workers/lib/usage/collectors/index.ts +31 -15
- package/templates/shared/workers/lib/usage/collectors/minimax.ts +106 -0
- package/templates/shared/workers/lib/usage/collectors/openai.ts +171 -0
- package/templates/shared/workers/lib/usage/collectors/resend.ts +79 -0
- package/templates/shared/workers/lib/usage/collectors/stripe.ts +192 -0
- package/templates/shared/workers/lib/usage/shared/types.ts +46 -0
- package/templates/shared/workers/platform-usage.ts +98 -8
- package/templates/standard/dashboard/src/components/errors/ErrorStats.tsx +53 -0
- package/templates/standard/dashboard/src/components/errors/ErrorsTable.tsx +133 -0
- package/templates/standard/dashboard/src/components/errors/index.ts +2 -0
- package/templates/standard/dashboard/src/components/health/CircuitBreakerEvents.tsx +69 -0
- package/templates/standard/dashboard/src/components/health/CircuitBreakerPanel.tsx +97 -0
- package/templates/standard/dashboard/src/components/health/DlqStatusCard.tsx +52 -0
- package/templates/standard/dashboard/src/components/health/HealthTabs.tsx +86 -0
- package/templates/standard/dashboard/src/components/health/index.ts +4 -0
- package/templates/standard/dashboard/src/lib/errors.ts +28 -0
- package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/mute.ts +49 -0
- package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/resolve.ts +36 -0
- package/templates/standard/dashboard/src/pages/api/errors/[fingerprint].ts +55 -0
- package/templates/standard/dashboard/src/pages/api/errors/index.ts +58 -0
- package/templates/standard/dashboard/src/pages/api/errors/stats.ts +55 -0
- package/templates/standard/dashboard/src/pages/api/health/audit-history.ts +37 -0
- package/templates/standard/dashboard/src/pages/api/health/dlq.ts +43 -0
- package/templates/standard/dashboard/src/pages/circuit-breakers.astro +13 -0
- package/templates/standard/dashboard/src/pages/errors.astro +13 -0
- package/templates/standard/dashboard/src/pages/health.astro +11 -0
- package/templates/standard/migrations/009_topology_mapper.sql +65 -0
- package/templates/standard/tests/unit/error-collector/capture.test.ts +106 -0
- package/templates/standard/tests/unit/error-collector/fingerprint.test.ts +155 -0
- package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +37 -3
- package/templates/standard/workers/lib/error-collector/gap-alerts.ts +32 -1
- package/templates/standard/workers/lib/mapper/attribution-check.ts +339 -0
- package/templates/standard/workers/lib/mapper/index.ts +7 -0
- package/templates/standard/workers/platform-mapper.ts +482 -0
- package/templates/standard/workers/platform-sdk-test-client.ts +125 -0
- package/templates/standard/wrangler.mapper.jsonc.hbs +85 -0
- package/templates/standard/wrangler.sdk-test-client.jsonc.hbs +62 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom HTTP Collector Factory
|
|
3
|
+
*
|
|
4
|
+
* Factory function for creating collectors that fetch metrics from arbitrary
|
|
5
|
+
* REST APIs. Use when no dedicated collector exists for your provider.
|
|
6
|
+
*
|
|
7
|
+
* Supports bearer token, basic auth, and API key header authentication.
|
|
8
|
+
* Extracts multiple metrics from a single JSON response using dot-notation paths.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { createCustomCollector } from './custom-http';
|
|
13
|
+
*
|
|
14
|
+
* const myCollector = createCustomCollector({
|
|
15
|
+
* name: 'my-api',
|
|
16
|
+
* url: 'https://api.example.com/v1/usage',
|
|
17
|
+
* auth: { type: 'bearer', envKey: 'MY_API_TOKEN' },
|
|
18
|
+
* metrics: {
|
|
19
|
+
* requests: { jsonPath: 'data.total_requests', unit: 'count' },
|
|
20
|
+
* cost: { jsonPath: 'data.billing.amount', unit: 'usd', isCost: true },
|
|
21
|
+
* },
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // Register in COLLECTORS array:
|
|
25
|
+
* // const COLLECTORS = [myCollector];
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { Env, CustomHttpCollectorConfig } from '../shared';
|
|
30
|
+
import { fetchWithRetry } from '../shared';
|
|
31
|
+
import { createLoggerFromEnv } from '@littlebearapps/platform-consumer-sdk';
|
|
32
|
+
import type { ExternalCollector } from './index';
|
|
33
|
+
|
|
34
|
+
/** Extracted metrics result from a custom collector */
|
|
35
|
+
export interface CustomMetrics {
|
|
36
|
+
/** Metric values keyed by name */
|
|
37
|
+
values: Record<string, number>;
|
|
38
|
+
/** Metadata about each metric */
|
|
39
|
+
meta: Record<string, { unit: string; isCost: boolean }>;
|
|
40
|
+
/** Source URL that was fetched */
|
|
41
|
+
source: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extract a value from a nested object using dot-notation path.
|
|
46
|
+
* Returns undefined if the path doesn't exist.
|
|
47
|
+
*/
|
|
48
|
+
function extractJsonPath(obj: unknown, path: string): unknown {
|
|
49
|
+
const parts = path.split('.');
|
|
50
|
+
let current: unknown = obj;
|
|
51
|
+
|
|
52
|
+
for (const part of parts) {
|
|
53
|
+
if (current === null || current === undefined || typeof current !== 'object') {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
current = (current as Record<string, unknown>)[part];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return current;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build request headers for the configured auth type.
|
|
64
|
+
*/
|
|
65
|
+
function buildAuthHeaders(
|
|
66
|
+
config: CustomHttpCollectorConfig,
|
|
67
|
+
env: Env
|
|
68
|
+
): Record<string, string> | null {
|
|
69
|
+
const headers: Record<string, string> = { ...config.headers };
|
|
70
|
+
|
|
71
|
+
if (config.auth.type === 'bearer') {
|
|
72
|
+
const token = (env as Record<string, unknown>)[config.auth.envKey] as string | undefined;
|
|
73
|
+
if (!token) return null;
|
|
74
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
75
|
+
} else if (config.auth.type === 'basic') {
|
|
76
|
+
const username = (env as Record<string, unknown>)[config.auth.usernameEnvKey] as
|
|
77
|
+
| string
|
|
78
|
+
| undefined;
|
|
79
|
+
const password = (env as Record<string, unknown>)[config.auth.passwordEnvKey] as
|
|
80
|
+
| string
|
|
81
|
+
| undefined;
|
|
82
|
+
if (!username || !password) return null;
|
|
83
|
+
headers['Authorization'] = `Basic ${btoa(`${username}:${password}`)}`;
|
|
84
|
+
} else if (config.auth.type === 'api-key') {
|
|
85
|
+
const key = (env as Record<string, unknown>)[config.auth.envKey] as string | undefined;
|
|
86
|
+
if (!key) return null;
|
|
87
|
+
headers[config.auth.headerName] = key;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return headers;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create a custom HTTP collector from a configuration object.
|
|
95
|
+
*
|
|
96
|
+
* The collector fetches the configured URL, parses the JSON response,
|
|
97
|
+
* and extracts metrics using the provided JSON paths.
|
|
98
|
+
*/
|
|
99
|
+
export function createCustomCollector(
|
|
100
|
+
config: CustomHttpCollectorConfig
|
|
101
|
+
): ExternalCollector<CustomMetrics | null> {
|
|
102
|
+
return {
|
|
103
|
+
name: config.name,
|
|
104
|
+
defaultValue: null,
|
|
105
|
+
collect: async (env: Env): Promise<CustomMetrics | null> => {
|
|
106
|
+
const log = createLoggerFromEnv(env, 'platform-usage', `platform:usage:${config.name}`);
|
|
107
|
+
|
|
108
|
+
const headers = buildAuthHeaders(config, env);
|
|
109
|
+
if (!headers) {
|
|
110
|
+
log.info(`No credentials configured for ${config.name}, skipping collection`);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const response = await fetchWithRetry(config.url, { headers });
|
|
116
|
+
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
const errorText = await response.text();
|
|
119
|
+
if (response.status === 401 || response.status === 403) {
|
|
120
|
+
log.info(`${config.name} API access denied`, { status: response.status });
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
log.error(`${config.name} API returned ${response.status}: ${errorText}`);
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const data = (await response.json()) as unknown;
|
|
128
|
+
|
|
129
|
+
const values: Record<string, number> = {};
|
|
130
|
+
const meta: Record<string, { unit: string; isCost: boolean }> = {};
|
|
131
|
+
|
|
132
|
+
for (const [metricName, metricConfig] of Object.entries(config.metrics)) {
|
|
133
|
+
const raw = extractJsonPath(data, metricConfig.jsonPath);
|
|
134
|
+
const value = typeof raw === 'number' ? raw : parseFloat(String(raw || '0'));
|
|
135
|
+
values[metricName] = isNaN(value) ? 0 : value;
|
|
136
|
+
meta[metricName] = { unit: metricConfig.unit, isCost: metricConfig.isCost ?? false };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
log.info(`Collected ${config.name} metrics`, {
|
|
140
|
+
metricCount: Object.keys(values).length,
|
|
141
|
+
values,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return { values, meta, source: config.url };
|
|
145
|
+
} catch (error) {
|
|
146
|
+
log.error(`Failed to collect ${config.name} metrics`, error);
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek Balance Collector
|
|
3
|
+
*
|
|
4
|
+
* Collects account balance from the DeepSeek API.
|
|
5
|
+
* This is a STOCK metric (balance remaining), not a FLOW metric (usage).
|
|
6
|
+
* Do NOT sum these values in dashboards - display as a gauge/runway indicator.
|
|
7
|
+
*
|
|
8
|
+
* @see https://api-docs.deepseek.com/api/get-user-balance
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Env, DeepSeekBalanceData } from '../shared';
|
|
12
|
+
import { fetchWithRetry } from '../shared';
|
|
13
|
+
import { createLoggerFromEnv } from '@littlebearapps/platform-consumer-sdk';
|
|
14
|
+
import type { ExternalCollector } from './index';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Collect DeepSeek account balance.
|
|
18
|
+
* Returns the current balance snapshot - this is NOT daily usage.
|
|
19
|
+
*/
|
|
20
|
+
export async function collectDeepSeekBalance(env: Env): Promise<DeepSeekBalanceData | null> {
|
|
21
|
+
const log = createLoggerFromEnv(env, 'platform-usage', 'platform:usage:deepseek');
|
|
22
|
+
if (!env.DEEPSEEK_API_KEY) {
|
|
23
|
+
log.info('No DEEPSEEK_API_KEY configured, skipping balance collection');
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const response = await fetchWithRetry('https://api.deepseek.com/user/balance', {
|
|
29
|
+
headers: {
|
|
30
|
+
Authorization: `Bearer ${env.DEEPSEEK_API_KEY}`,
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
const errorText = await response.text();
|
|
37
|
+
if (response.status === 401 || response.status === 403) {
|
|
38
|
+
log.info('Balance API access denied - check API key permissions', {
|
|
39
|
+
status: response.status,
|
|
40
|
+
});
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
log.error(`Balance API returned ${response.status}: ${errorText}`);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const data = (await response.json()) as {
|
|
48
|
+
is_available?: boolean;
|
|
49
|
+
balance_infos?: Array<{
|
|
50
|
+
currency?: string;
|
|
51
|
+
total_balance?: string;
|
|
52
|
+
granted_balance?: string;
|
|
53
|
+
topped_up_balance?: string;
|
|
54
|
+
}>;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Find USD balance (or first available)
|
|
58
|
+
const balanceInfo =
|
|
59
|
+
data.balance_infos?.find((b) => b.currency === 'USD') || data.balance_infos?.[0];
|
|
60
|
+
|
|
61
|
+
if (!balanceInfo) {
|
|
62
|
+
log.info('No balance info returned from API');
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const result: DeepSeekBalanceData = {
|
|
67
|
+
totalBalance: parseFloat(balanceInfo.total_balance || '0'),
|
|
68
|
+
grantedBalance: parseFloat(balanceInfo.granted_balance || '0'),
|
|
69
|
+
toppedUpBalance: parseFloat(balanceInfo.topped_up_balance || '0'),
|
|
70
|
+
isAvailable: data.is_available ?? false,
|
|
71
|
+
currency: balanceInfo.currency || 'USD',
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
log.info('Collected DeepSeek balance', {
|
|
75
|
+
totalBalance: result.totalBalance,
|
|
76
|
+
currency: result.currency,
|
|
77
|
+
isAvailable: result.isAvailable,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return result;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
log.error('Failed to collect DeepSeek balance', error);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Collector registration for use in collectors/index.ts COLLECTORS array */
|
|
88
|
+
export const deepseekCollector: ExternalCollector<DeepSeekBalanceData | null> = {
|
|
89
|
+
name: 'deepseek',
|
|
90
|
+
collect: collectDeepSeekBalance,
|
|
91
|
+
defaultValue: null,
|
|
92
|
+
};
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Gemini Usage Collector
|
|
3
|
+
*
|
|
4
|
+
* Collects API usage metrics from Google Cloud Monitoring API.
|
|
5
|
+
* This is a FLOW metric (actual usage) - safe to sum/aggregate.
|
|
6
|
+
*
|
|
7
|
+
* Authentication uses native Web Crypto API to sign JWTs for service account auth.
|
|
8
|
+
* Does NOT require googleapis or jsonwebtoken packages.
|
|
9
|
+
*
|
|
10
|
+
* Groups by API method (GenerateContent, EmbedContent, etc.) for breakdown.
|
|
11
|
+
* Includes cost estimation based on Gemini 2.0 Flash pricing.
|
|
12
|
+
*
|
|
13
|
+
* Advanced: Requires GCP service account with Cloud Monitoring read access.
|
|
14
|
+
*
|
|
15
|
+
* @see https://cloud.google.com/monitoring/api/ref_v3/rest/v3/projects.timeSeries/query
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { Env, GeminiUsageData } from '../shared';
|
|
19
|
+
import { createLoggerFromEnv } from '@littlebearapps/platform-consumer-sdk';
|
|
20
|
+
import type { ExternalCollector } from './index';
|
|
21
|
+
|
|
22
|
+
/** Service account credentials structure (from GCP JSON key file) */
|
|
23
|
+
interface ServiceAccountCredentials {
|
|
24
|
+
type: string;
|
|
25
|
+
project_id: string;
|
|
26
|
+
private_key_id: string;
|
|
27
|
+
private_key: string;
|
|
28
|
+
client_email: string;
|
|
29
|
+
client_id: string;
|
|
30
|
+
auth_uri: string;
|
|
31
|
+
token_uri: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Convert a PEM-encoded private key to a CryptoKey for signing. */
|
|
35
|
+
async function importPrivateKey(pem: string): Promise<CryptoKey> {
|
|
36
|
+
const pemContents = pem
|
|
37
|
+
.replace(/-----BEGIN PRIVATE KEY-----/, '')
|
|
38
|
+
.replace(/-----END PRIVATE KEY-----/, '')
|
|
39
|
+
.replace(/\s/g, '');
|
|
40
|
+
|
|
41
|
+
const binaryString = atob(pemContents);
|
|
42
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
43
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
44
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return crypto.subtle.importKey(
|
|
48
|
+
'pkcs8',
|
|
49
|
+
bytes.buffer,
|
|
50
|
+
{ name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
|
|
51
|
+
false,
|
|
52
|
+
['sign']
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Base64URL encode (for JWT) */
|
|
57
|
+
function base64UrlEncode(data: string | ArrayBuffer): string {
|
|
58
|
+
let base64: string;
|
|
59
|
+
if (typeof data === 'string') {
|
|
60
|
+
base64 = btoa(data);
|
|
61
|
+
} else {
|
|
62
|
+
const bytes = new Uint8Array(data);
|
|
63
|
+
let binary = '';
|
|
64
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
65
|
+
binary += String.fromCharCode(bytes[i]);
|
|
66
|
+
}
|
|
67
|
+
base64 = btoa(binary);
|
|
68
|
+
}
|
|
69
|
+
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Create a signed JWT for Google service account authentication. */
|
|
73
|
+
async function createServiceAccountJwt(credentials: ServiceAccountCredentials): Promise<string> {
|
|
74
|
+
const now = Math.floor(Date.now() / 1000);
|
|
75
|
+
|
|
76
|
+
const header = { alg: 'RS256', typ: 'JWT' };
|
|
77
|
+
const payload = {
|
|
78
|
+
iss: credentials.client_email,
|
|
79
|
+
sub: credentials.client_email,
|
|
80
|
+
aud: 'https://oauth2.googleapis.com/token',
|
|
81
|
+
iat: now,
|
|
82
|
+
exp: now + 3600,
|
|
83
|
+
scope: 'https://www.googleapis.com/auth/monitoring.read',
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const headerB64 = base64UrlEncode(JSON.stringify(header));
|
|
87
|
+
const payloadB64 = base64UrlEncode(JSON.stringify(payload));
|
|
88
|
+
const unsignedToken = `${headerB64}.${payloadB64}`;
|
|
89
|
+
|
|
90
|
+
const privateKey = await importPrivateKey(credentials.private_key);
|
|
91
|
+
const encoder = new TextEncoder();
|
|
92
|
+
const signature = await crypto.subtle.sign(
|
|
93
|
+
'RSASSA-PKCS1-v1_5',
|
|
94
|
+
privateKey,
|
|
95
|
+
encoder.encode(unsignedToken)
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
return `${unsignedToken}.${base64UrlEncode(signature)}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Exchange JWT for an access token from Google OAuth2. */
|
|
102
|
+
async function getAccessToken(credentials: ServiceAccountCredentials): Promise<string | null> {
|
|
103
|
+
try {
|
|
104
|
+
const jwt = await createServiceAccountJwt(credentials);
|
|
105
|
+
|
|
106
|
+
const response = await fetch('https://oauth2.googleapis.com/token', {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
109
|
+
body: new URLSearchParams({
|
|
110
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
|
111
|
+
assertion: jwt,
|
|
112
|
+
}),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const data = (await response.json()) as { access_token?: string };
|
|
120
|
+
return data.access_token || null;
|
|
121
|
+
} catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Gemini 2.0 Flash pricing: ~$0.075 per 1K requests (rough average)
|
|
127
|
+
const GEMINI_COST_PER_1K_REQUESTS = 0.075;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Collect Google Gemini API usage from Cloud Monitoring.
|
|
131
|
+
* Returns request counts grouped by API method for the last 24 hours.
|
|
132
|
+
*/
|
|
133
|
+
export async function collectGeminiUsage(env: Env): Promise<GeminiUsageData | null> {
|
|
134
|
+
const log = createLoggerFromEnv(env, 'platform-usage', 'platform:usage:gemini');
|
|
135
|
+
|
|
136
|
+
if (!env.GCP_PROJECT_ID || !env.GCP_SERVICE_ACCOUNT_JSON) {
|
|
137
|
+
log.info('No GCP credentials configured, skipping Gemini usage collection');
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let credentials: ServiceAccountCredentials;
|
|
142
|
+
try {
|
|
143
|
+
credentials = JSON.parse(env.GCP_SERVICE_ACCOUNT_JSON) as ServiceAccountCredentials;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
log.error('Failed to parse GCP_SERVICE_ACCOUNT_JSON', error);
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const accessToken = await getAccessToken(credentials);
|
|
151
|
+
if (!accessToken) {
|
|
152
|
+
log.error('Failed to obtain Google Cloud access token');
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const now = new Date();
|
|
157
|
+
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
158
|
+
|
|
159
|
+
// MQL query for generativelanguage.googleapis.com request counts
|
|
160
|
+
const mqlQuery = `
|
|
161
|
+
fetch consumed_api
|
|
162
|
+
| metric 'serviceruntime.googleapis.com/api/request_count'
|
|
163
|
+
| filter resource.service == 'generativelanguage.googleapis.com'
|
|
164
|
+
| group_by [resource.method]
|
|
165
|
+
| align rate(1d)
|
|
166
|
+
| every 1d
|
|
167
|
+
`.trim();
|
|
168
|
+
|
|
169
|
+
const response = await fetch(
|
|
170
|
+
`https://monitoring.googleapis.com/v3/projects/${env.GCP_PROJECT_ID}/timeSeries:query`,
|
|
171
|
+
{
|
|
172
|
+
method: 'POST',
|
|
173
|
+
headers: {
|
|
174
|
+
Authorization: `Bearer ${accessToken}`,
|
|
175
|
+
'Content-Type': 'application/json',
|
|
176
|
+
},
|
|
177
|
+
body: JSON.stringify({ query: mqlQuery }),
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
const errorText = await response.text();
|
|
183
|
+
if (response.status === 401 || response.status === 403) {
|
|
184
|
+
log.info('Cloud Monitoring API access denied - check service account permissions', {
|
|
185
|
+
status: response.status,
|
|
186
|
+
});
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
log.error(`Cloud Monitoring API returned ${response.status}: ${errorText}`);
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const data = (await response.json()) as {
|
|
194
|
+
timeSeriesData?: Array<{
|
|
195
|
+
labelValues?: Array<{ stringValue?: string }>;
|
|
196
|
+
pointData?: Array<{
|
|
197
|
+
values?: Array<{ doubleValue?: number; int64Value?: string }>;
|
|
198
|
+
timeInterval?: { startTime?: string; endTime?: string };
|
|
199
|
+
}>;
|
|
200
|
+
}>;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
let requestCount = 0;
|
|
204
|
+
let periodStart = yesterday.toISOString();
|
|
205
|
+
let periodEnd = now.toISOString();
|
|
206
|
+
const methodBreakdown: Record<string, number> = {};
|
|
207
|
+
|
|
208
|
+
if (data.timeSeriesData) {
|
|
209
|
+
for (const ts of data.timeSeriesData) {
|
|
210
|
+
const method = ts.labelValues?.[0]?.stringValue || 'unknown';
|
|
211
|
+
let methodCount = 0;
|
|
212
|
+
|
|
213
|
+
if (ts.pointData) {
|
|
214
|
+
for (const point of ts.pointData) {
|
|
215
|
+
if (point.values?.[0]) {
|
|
216
|
+
const value = point.values[0];
|
|
217
|
+
const count = value.doubleValue || parseInt(value.int64Value || '0', 10);
|
|
218
|
+
requestCount += count;
|
|
219
|
+
methodCount += count;
|
|
220
|
+
}
|
|
221
|
+
if (point.timeInterval) {
|
|
222
|
+
periodStart = point.timeInterval.startTime || periodStart;
|
|
223
|
+
periodEnd = point.timeInterval.endTime || periodEnd;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (methodCount > 0) {
|
|
229
|
+
methodBreakdown[method] = (methodBreakdown[method] || 0) + methodCount;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const estimatedCostUsd = (requestCount / 1000) * GEMINI_COST_PER_1K_REQUESTS;
|
|
235
|
+
|
|
236
|
+
const result: GeminiUsageData = {
|
|
237
|
+
requestCount,
|
|
238
|
+
avgLatencyMs: 0, // Would need separate query for latency metrics
|
|
239
|
+
estimatedCostUsd,
|
|
240
|
+
periodStart,
|
|
241
|
+
periodEnd,
|
|
242
|
+
methodBreakdown,
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
log.info('Collected Gemini usage', {
|
|
246
|
+
requestCount: result.requestCount,
|
|
247
|
+
estimatedCostUsd: result.estimatedCostUsd,
|
|
248
|
+
methods: Object.keys(methodBreakdown).length,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
return result;
|
|
252
|
+
} catch (error) {
|
|
253
|
+
log.error('Failed to collect Gemini usage', error);
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Collector registration for use in collectors/index.ts COLLECTORS array */
|
|
259
|
+
export const geminiCollector: ExternalCollector<GeminiUsageData | null> = {
|
|
260
|
+
name: 'gemini',
|
|
261
|
+
collect: collectGeminiUsage,
|
|
262
|
+
defaultValue: null,
|
|
263
|
+
};
|