deepclaw-openclaw 0.1.1 → 0.1.3
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/CHANGELOG.md +5 -0
- package/README.md +15 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +20 -0
- package/dist/src/config.d.ts +9 -0
- package/dist/src/config.js +13 -0
- package/dist/src/pricing.d.ts +38 -0
- package/dist/src/pricing.js +146 -0
- package/dist/src/service.d.ts +33 -0
- package/dist/src/service.js +150 -0
- package/openclaw.plugin.json +3 -1
- package/package.json +10 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented here.
|
|
4
4
|
|
|
5
|
+
## 0.1.2 - GitHub Packages publishing prep
|
|
6
|
+
|
|
7
|
+
- Add GitHub Actions workflow support for publishing to npmjs and GitHub Packages.
|
|
8
|
+
- Document the scoped GitHub Packages install path as `@digitizers/deepclaw-openclaw`.
|
|
9
|
+
|
|
5
10
|
## 0.1.1 - Metadata cleanup
|
|
6
11
|
|
|
7
12
|
- Mark `syncToken` as required in the OpenClaw plugin config schema for clearer marketplace/security metadata.
|
package/README.md
CHANGED
|
@@ -27,10 +27,24 @@ This plugin captures the raw usage shape exposed by OpenClaw, normalizes the imp
|
|
|
27
27
|
|
|
28
28
|
## Installation
|
|
29
29
|
|
|
30
|
+
From the public npm registry:
|
|
31
|
+
|
|
30
32
|
```bash
|
|
31
33
|
npm install deepclaw-openclaw
|
|
32
34
|
```
|
|
33
35
|
|
|
36
|
+
From GitHub Packages, once the scoped mirror package has been published:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# .npmrc
|
|
40
|
+
@digitizers:registry=https://npm.pkg.github.com
|
|
41
|
+
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
|
|
42
|
+
|
|
43
|
+
npm install @digitizers/deepclaw-openclaw
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The GitHub Packages build is published as the scoped alias `@digitizers/deepclaw-openclaw`; the canonical npmjs package remains `deepclaw-openclaw` for easier public installation.
|
|
47
|
+
|
|
34
48
|
For local development or manual installation:
|
|
35
49
|
|
|
36
50
|
```bash
|
|
@@ -128,7 +142,7 @@ Do not commit sync tokens or OpenClaw runtime state. See [SECURITY.md](SECURITY.
|
|
|
128
142
|
|
|
129
143
|
## Status
|
|
130
144
|
|
|
131
|
-
`0.1.
|
|
145
|
+
`0.1.2` is an initial public release candidate. APIs may still evolve with OpenClaw plugin hook changes.
|
|
132
146
|
|
|
133
147
|
## License
|
|
134
148
|
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
declare const plugin: {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
configSchema: import("openclaw/plugin-sdk").OpenClawPluginConfigSchema;
|
|
7
|
+
register(api: OpenClawPluginApi): void;
|
|
8
|
+
};
|
|
9
|
+
export default plugin;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
2
|
+
import { createDeepClawService } from "./src/service.js";
|
|
3
|
+
import { parseDeepClawConfig } from "./src/config.js";
|
|
4
|
+
const plugin = {
|
|
5
|
+
id: "deepclaw-openclaw",
|
|
6
|
+
name: "DeepClaw",
|
|
7
|
+
description: "Export real-time LLM cost & usage data to DeepClaw analytics",
|
|
8
|
+
configSchema: emptyPluginConfigSchema(),
|
|
9
|
+
register(api) {
|
|
10
|
+
const config = parseDeepClawConfig(api.pluginConfig);
|
|
11
|
+
if (!config.enabled || !config.syncToken) {
|
|
12
|
+
api.logger.info("[deepclaw] plugin loaded but disabled (set enabled=true and syncToken in config)");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const service = createDeepClawService(api, config);
|
|
16
|
+
service.registerHooks();
|
|
17
|
+
api.logger.info(`[deepclaw] started — tracking to ${config.apiUrl}`);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
export default plugin;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function parseDeepClawConfig(raw) {
|
|
2
|
+
const cfg = (raw ?? {});
|
|
3
|
+
return {
|
|
4
|
+
enabled: cfg["enabled"] !== false && cfg["enabled"] !== undefined
|
|
5
|
+
? Boolean(cfg["enabled"])
|
|
6
|
+
: false,
|
|
7
|
+
apiUrl: String(cfg["apiUrl"] ?? process.env["DEEPCLAW_API_URL"] ?? "https://app.deep-claw.com"),
|
|
8
|
+
syncToken: String(cfg["syncToken"] ?? process.env["DEEPCLAW_SYNC_TOKEN"] ?? ""),
|
|
9
|
+
instanceId: String(cfg["instanceId"] ?? process.env["DEEPCLAW_INSTANCE_ID"] ?? "default"),
|
|
10
|
+
flushIntervalMs: Number(cfg["flushIntervalMs"] ?? 5000),
|
|
11
|
+
debug: Boolean(cfg["debug"] ?? false),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM pricing table — cost per 1M tokens in USD.
|
|
3
|
+
* Source: official provider pricing pages (updated 2025-04).
|
|
4
|
+
*
|
|
5
|
+
* For Gemini: step-function tiered pricing (NOT graduated).
|
|
6
|
+
* If promptTokenCount exceeds the threshold, ALL tokens are billed at the higher rate.
|
|
7
|
+
*/
|
|
8
|
+
export interface ModelPricing {
|
|
9
|
+
inputPerM: number;
|
|
10
|
+
outputPerM: number;
|
|
11
|
+
cacheReadPerM?: number;
|
|
12
|
+
cacheWritePerM?: number;
|
|
13
|
+
/** If set, all tokens are billed at higher rates when promptTokens > this */
|
|
14
|
+
tierThreshold?: number;
|
|
15
|
+
inputPerMAboveTier?: number;
|
|
16
|
+
outputPerMAboveTier?: number;
|
|
17
|
+
cacheReadPerMAboveTier?: number;
|
|
18
|
+
/** Separate rate for thinking tokens (e.g. Gemini 2.5) */
|
|
19
|
+
thinkingOutputPerM?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface CostBreakdown {
|
|
22
|
+
inputCost: number;
|
|
23
|
+
outputCost: number;
|
|
24
|
+
thinkingCost: number;
|
|
25
|
+
cacheReadCost: number;
|
|
26
|
+
cacheWriteCost: number;
|
|
27
|
+
totalCost: number;
|
|
28
|
+
priceSource: "table" | "unknown";
|
|
29
|
+
modelKey: string | null;
|
|
30
|
+
}
|
|
31
|
+
export declare function calculateCost(opts: {
|
|
32
|
+
model: string;
|
|
33
|
+
inputTokens: number;
|
|
34
|
+
outputTokens: number;
|
|
35
|
+
cacheReadTokens?: number;
|
|
36
|
+
cacheWriteTokens?: number;
|
|
37
|
+
thinkingTokens?: number;
|
|
38
|
+
}): CostBreakdown;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM pricing table — cost per 1M tokens in USD.
|
|
3
|
+
* Source: official provider pricing pages (updated 2025-04).
|
|
4
|
+
*
|
|
5
|
+
* For Gemini: step-function tiered pricing (NOT graduated).
|
|
6
|
+
* If promptTokenCount exceeds the threshold, ALL tokens are billed at the higher rate.
|
|
7
|
+
*/
|
|
8
|
+
const PRICING = {
|
|
9
|
+
// ── Gemini ───────────────────────────────────────────────────────────────
|
|
10
|
+
"gemini-2.5-flash": {
|
|
11
|
+
inputPerM: 0.15,
|
|
12
|
+
outputPerM: 0.60,
|
|
13
|
+
cacheReadPerM: 0.0375,
|
|
14
|
+
cacheWritePerM: 1.00,
|
|
15
|
+
thinkingOutputPerM: 3.50,
|
|
16
|
+
tierThreshold: 200_000,
|
|
17
|
+
inputPerMAboveTier: 0.30,
|
|
18
|
+
outputPerMAboveTier: 1.20,
|
|
19
|
+
cacheReadPerMAboveTier: 0.075,
|
|
20
|
+
},
|
|
21
|
+
"gemini-2.5-pro": {
|
|
22
|
+
inputPerM: 1.25,
|
|
23
|
+
outputPerM: 10.00,
|
|
24
|
+
cacheReadPerM: 0.31,
|
|
25
|
+
tierThreshold: 200_000,
|
|
26
|
+
inputPerMAboveTier: 2.50,
|
|
27
|
+
outputPerMAboveTier: 15.00,
|
|
28
|
+
cacheReadPerMAboveTier: 0.63,
|
|
29
|
+
},
|
|
30
|
+
"gemini-2.0-flash": {
|
|
31
|
+
inputPerM: 0.10,
|
|
32
|
+
outputPerM: 0.40,
|
|
33
|
+
cacheReadPerM: 0.025,
|
|
34
|
+
},
|
|
35
|
+
"gemini-1.5-pro": {
|
|
36
|
+
inputPerM: 1.25,
|
|
37
|
+
outputPerM: 5.00,
|
|
38
|
+
cacheReadPerM: 0.3125,
|
|
39
|
+
tierThreshold: 128_000,
|
|
40
|
+
inputPerMAboveTier: 2.50,
|
|
41
|
+
outputPerMAboveTier: 10.00,
|
|
42
|
+
cacheReadPerMAboveTier: 0.625,
|
|
43
|
+
},
|
|
44
|
+
"gemini-1.5-flash": {
|
|
45
|
+
inputPerM: 0.075,
|
|
46
|
+
outputPerM: 0.30,
|
|
47
|
+
cacheReadPerM: 0.01875,
|
|
48
|
+
tierThreshold: 128_000,
|
|
49
|
+
inputPerMAboveTier: 0.15,
|
|
50
|
+
outputPerMAboveTier: 0.60,
|
|
51
|
+
cacheReadPerMAboveTier: 0.0375,
|
|
52
|
+
},
|
|
53
|
+
// ── OpenAI ───────────────────────────────────────────────────────────────
|
|
54
|
+
"gpt-4o": { inputPerM: 2.50, outputPerM: 10.00, cacheReadPerM: 1.25 },
|
|
55
|
+
"gpt-4o-mini": { inputPerM: 0.15, outputPerM: 0.60, cacheReadPerM: 0.075 },
|
|
56
|
+
"gpt-4.1": { inputPerM: 2.00, outputPerM: 8.00, cacheReadPerM: 0.50 },
|
|
57
|
+
"gpt-4.1-mini": { inputPerM: 0.40, outputPerM: 1.60, cacheReadPerM: 0.10 },
|
|
58
|
+
"gpt-5": { inputPerM: 10.00, outputPerM: 40.00, cacheReadPerM: 2.50 },
|
|
59
|
+
"gpt-5.4": { inputPerM: 10.00, outputPerM: 40.00, cacheReadPerM: 2.50 },
|
|
60
|
+
"gpt-5.4-mini": { inputPerM: 0.40, outputPerM: 1.60, cacheReadPerM: 0.10 },
|
|
61
|
+
"o3": { inputPerM: 10.00, outputPerM: 40.00, cacheReadPerM: 2.50 },
|
|
62
|
+
"o4-mini": { inputPerM: 1.10, outputPerM: 4.40, cacheReadPerM: 0.275 },
|
|
63
|
+
// ── Anthropic ────────────────────────────────────────────────────────────
|
|
64
|
+
"claude-opus-4": { inputPerM: 15.00, outputPerM: 75.00, cacheReadPerM: 1.50, cacheWritePerM: 18.75 },
|
|
65
|
+
"claude-opus-4-5": { inputPerM: 15.00, outputPerM: 75.00, cacheReadPerM: 1.50, cacheWritePerM: 18.75 },
|
|
66
|
+
"claude-opus-4-6": { inputPerM: 15.00, outputPerM: 75.00, cacheReadPerM: 1.50, cacheWritePerM: 18.75 },
|
|
67
|
+
"claude-sonnet-4": { inputPerM: 3.00, outputPerM: 15.00, cacheReadPerM: 0.30, cacheWritePerM: 3.75 },
|
|
68
|
+
"claude-sonnet-4-5": { inputPerM: 3.00, outputPerM: 15.00, cacheReadPerM: 0.30, cacheWritePerM: 3.75 },
|
|
69
|
+
"claude-sonnet-4-6": { inputPerM: 3.00, outputPerM: 15.00, cacheReadPerM: 0.30, cacheWritePerM: 3.75 },
|
|
70
|
+
"claude-haiku-3-5": { inputPerM: 0.80, outputPerM: 4.00, cacheReadPerM: 0.08, cacheWritePerM: 1.00 },
|
|
71
|
+
// ── DeepSeek ─────────────────────────────────────────────────────────────
|
|
72
|
+
"deepseek-chat": { inputPerM: 0.27, outputPerM: 1.10, cacheReadPerM: 0.07 },
|
|
73
|
+
"deepseek-reasoner": { inputPerM: 0.55, outputPerM: 2.19, cacheReadPerM: 0.14 },
|
|
74
|
+
// ── xAI ──────────────────────────────────────────────────────────────────
|
|
75
|
+
"grok-3": { inputPerM: 3.00, outputPerM: 15.00 },
|
|
76
|
+
"grok-3-mini": { inputPerM: 0.30, outputPerM: 0.50 },
|
|
77
|
+
"grok-2-vision-1212": { inputPerM: 2.00, outputPerM: 10.00 },
|
|
78
|
+
};
|
|
79
|
+
/** Normalize model name for lookup (strip provider prefix, version suffixes like -latest) */
|
|
80
|
+
function normalizeModel(raw) {
|
|
81
|
+
const lower = raw.toLowerCase();
|
|
82
|
+
// Strip provider prefix (e.g. "google/gemini-2.5-flash" → "gemini-2.5-flash")
|
|
83
|
+
const slash = lower.lastIndexOf("/");
|
|
84
|
+
const name = slash >= 0 ? lower.slice(slash + 1) : lower;
|
|
85
|
+
// Strip common suffixes
|
|
86
|
+
return name
|
|
87
|
+
.replace(/-latest$/, "")
|
|
88
|
+
.replace(/-\d{8}$/, "") // date suffixes like -20241022
|
|
89
|
+
.replace(/-exp$/, "")
|
|
90
|
+
.replace(/-preview$/, "");
|
|
91
|
+
}
|
|
92
|
+
export function calculateCost(opts) {
|
|
93
|
+
const key = normalizeModel(opts.model);
|
|
94
|
+
// Try exact match, then longest prefix match for dated/preview provider aliases.
|
|
95
|
+
let modelKey = PRICING[key] ? key : null;
|
|
96
|
+
let pricing = modelKey ? PRICING[modelKey] : undefined;
|
|
97
|
+
if (!pricing) {
|
|
98
|
+
modelKey = Object.keys(PRICING)
|
|
99
|
+
.filter((k) => key.startsWith(k) || k.startsWith(key))
|
|
100
|
+
.sort((a, b) => b.length - a.length)[0] ?? null;
|
|
101
|
+
pricing = modelKey ? PRICING[modelKey] : undefined;
|
|
102
|
+
}
|
|
103
|
+
if (!pricing) {
|
|
104
|
+
return {
|
|
105
|
+
inputCost: 0, outputCost: 0,
|
|
106
|
+
thinkingCost: 0,
|
|
107
|
+
cacheReadCost: 0, cacheWriteCost: 0,
|
|
108
|
+
totalCost: 0,
|
|
109
|
+
priceSource: "unknown",
|
|
110
|
+
modelKey: null,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const M = 1_000_000;
|
|
114
|
+
const totalInput = opts.inputTokens;
|
|
115
|
+
// Gemini step-function tier: if total prompt > threshold, ALL tokens at higher rate
|
|
116
|
+
const aboveTier = pricing.tierThreshold && totalInput > pricing.tierThreshold;
|
|
117
|
+
const inputRate = aboveTier ? (pricing.inputPerMAboveTier ?? pricing.inputPerM) : pricing.inputPerM;
|
|
118
|
+
const outputRate = aboveTier ? (pricing.outputPerMAboveTier ?? pricing.outputPerM) : pricing.outputPerM;
|
|
119
|
+
const cacheReadRate = aboveTier
|
|
120
|
+
? (pricing.cacheReadPerMAboveTier ?? pricing.cacheReadPerM ?? 0)
|
|
121
|
+
: (pricing.cacheReadPerM ?? 0);
|
|
122
|
+
const cacheReadTokens = opts.cacheReadTokens ?? 0;
|
|
123
|
+
const cacheWriteTokens = opts.cacheWriteTokens ?? 0;
|
|
124
|
+
const thinkingTokens = opts.thinkingTokens ?? 0;
|
|
125
|
+
// For cached input: pay cache-read rate instead of full input rate
|
|
126
|
+
const nonCachedInput = Math.max(0, totalInput - cacheReadTokens);
|
|
127
|
+
const inputCost = (nonCachedInput / M) * inputRate;
|
|
128
|
+
const cacheReadCost = (cacheReadTokens / M) * cacheReadRate;
|
|
129
|
+
const cacheWriteCost = (cacheWriteTokens / M) * (pricing.cacheWritePerM ?? inputRate);
|
|
130
|
+
// Thinking vs normal output (additive — Gemini separates candidatesTokenCount and thoughtsTokenCount)
|
|
131
|
+
const outputCost = (opts.outputTokens / M) * outputRate;
|
|
132
|
+
const thinkingCost = pricing.thinkingOutputPerM
|
|
133
|
+
? (thinkingTokens / M) * pricing.thinkingOutputPerM
|
|
134
|
+
: (thinkingTokens / M) * outputRate;
|
|
135
|
+
const totalCost = inputCost + outputCost + thinkingCost + cacheReadCost + cacheWriteCost;
|
|
136
|
+
return {
|
|
137
|
+
inputCost,
|
|
138
|
+
outputCost,
|
|
139
|
+
thinkingCost,
|
|
140
|
+
cacheReadCost,
|
|
141
|
+
cacheWriteCost,
|
|
142
|
+
totalCost,
|
|
143
|
+
priceSource: "table",
|
|
144
|
+
modelKey,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { DeepClawConfig } from "./config.js";
|
|
3
|
+
/**
|
|
4
|
+
* One LLM call record — captured from llm_output hook.
|
|
5
|
+
* usage.total comes directly from the provider (not estimated).
|
|
6
|
+
*/
|
|
7
|
+
export interface LlmRecord {
|
|
8
|
+
runId: string;
|
|
9
|
+
sessionId: string;
|
|
10
|
+
provider: string;
|
|
11
|
+
model: string;
|
|
12
|
+
tokensIn: number;
|
|
13
|
+
tokensOut: number;
|
|
14
|
+
cacheRead: number;
|
|
15
|
+
cacheWrite: number;
|
|
16
|
+
thinkingTokens: number;
|
|
17
|
+
/** Calculated cost in USD */
|
|
18
|
+
costUsd: number;
|
|
19
|
+
costSource: "table" | "unknown";
|
|
20
|
+
/** Per-component breakdown */
|
|
21
|
+
costBreakdown?: {
|
|
22
|
+
input: number;
|
|
23
|
+
output: number;
|
|
24
|
+
cacheRead: number;
|
|
25
|
+
cacheWrite: number;
|
|
26
|
+
thinking: number;
|
|
27
|
+
};
|
|
28
|
+
timestamp: number;
|
|
29
|
+
}
|
|
30
|
+
export declare function createDeepClawService(api: OpenClawPluginApi, config: DeepClawConfig): {
|
|
31
|
+
registerHooks(): void;
|
|
32
|
+
shutdown(): Promise<void>;
|
|
33
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { calculateCost } from "./pricing.js";
|
|
2
|
+
export function createDeepClawService(api, config) {
|
|
3
|
+
const logger = api.logger;
|
|
4
|
+
const sessions = new Map();
|
|
5
|
+
let flushTimer;
|
|
6
|
+
// ─── helpers ────────────────────────────────────────────────────
|
|
7
|
+
function getOrCreateSession(sessionId) {
|
|
8
|
+
let acc = sessions.get(sessionId);
|
|
9
|
+
if (!acc) {
|
|
10
|
+
acc = { sessionId, records: [], startedAt: Date.now(), lastSeenAt: Date.now() };
|
|
11
|
+
sessions.set(sessionId, acc);
|
|
12
|
+
}
|
|
13
|
+
acc.lastSeenAt = Date.now();
|
|
14
|
+
return acc;
|
|
15
|
+
}
|
|
16
|
+
async function postToDeepClaw(path, body) {
|
|
17
|
+
const url = `${config.apiUrl.replace(/\/$/, "")}${path}`;
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetch(url, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: {
|
|
22
|
+
"Content-Type": "application/json",
|
|
23
|
+
Authorization: `Bearer ${config.syncToken}`,
|
|
24
|
+
},
|
|
25
|
+
body: JSON.stringify(body),
|
|
26
|
+
});
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
logger.warn(`[deepclaw] POST ${path} → ${res.status}: ${await res.text().catch(() => "")}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
logger.warn(`[deepclaw] POST ${path} failed: ${String(err)}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function flushSession(sessionId, reason) {
|
|
36
|
+
const acc = sessions.get(sessionId);
|
|
37
|
+
if (!acc || acc.records.length === 0)
|
|
38
|
+
return;
|
|
39
|
+
const totalCostUsd = acc.records.reduce((sum, r) => sum + (r.costUsd ?? 0), 0);
|
|
40
|
+
const totalIn = acc.records.reduce((sum, r) => sum + r.tokensIn, 0);
|
|
41
|
+
const totalOut = acc.records.reduce((sum, r) => sum + r.tokensOut, 0);
|
|
42
|
+
const totalCacheRead = acc.records.reduce((sum, r) => sum + r.cacheRead, 0);
|
|
43
|
+
const totalCacheWrite = acc.records.reduce((sum, r) => sum + r.cacheWrite, 0);
|
|
44
|
+
const totalThinking = acc.records.reduce((sum, r) => sum + r.thinkingTokens, 0);
|
|
45
|
+
const payload = {
|
|
46
|
+
instanceId: config.instanceId,
|
|
47
|
+
sessionId,
|
|
48
|
+
flushReason: reason,
|
|
49
|
+
llmCalls: acc.records,
|
|
50
|
+
summary: {
|
|
51
|
+
totalCostUsd,
|
|
52
|
+
costSource: acc.records.some((r) => r.costSource === "table") ? "table" : "unknown",
|
|
53
|
+
totalTokensIn: totalIn,
|
|
54
|
+
totalTokensOut: totalOut,
|
|
55
|
+
totalCacheRead,
|
|
56
|
+
totalCacheWrite,
|
|
57
|
+
totalThinkingTokens: totalThinking,
|
|
58
|
+
callCount: acc.records.length,
|
|
59
|
+
startedAt: acc.startedAt,
|
|
60
|
+
lastSeenAt: acc.lastSeenAt,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
if (config.debug) {
|
|
64
|
+
logger.info(`[deepclaw] flushing session ${sessionId}: ${acc.records.length} calls, $${totalCostUsd.toFixed(6)}`);
|
|
65
|
+
}
|
|
66
|
+
await postToDeepClaw("/api/sync/session", payload);
|
|
67
|
+
if (reason === "session_end") {
|
|
68
|
+
sessions.delete(sessionId);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// Keep session alive but clear records to avoid double-counting
|
|
72
|
+
acc.records = [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function flushAll(reason) {
|
|
76
|
+
const ids = Array.from(sessions.keys());
|
|
77
|
+
await Promise.all(ids.map((id) => flushSession(id, reason)));
|
|
78
|
+
}
|
|
79
|
+
// ─── public ─────────────────────────────────────────────────────
|
|
80
|
+
return {
|
|
81
|
+
registerHooks() {
|
|
82
|
+
// Capture actual cost from provider on every LLM completion
|
|
83
|
+
api.on("llm_output", async (event) => {
|
|
84
|
+
const tokensIn = event.usage?.input ?? 0;
|
|
85
|
+
const tokensOut = event.usage?.output ?? 0;
|
|
86
|
+
const cacheRead = event.usage?.cacheRead ?? 0;
|
|
87
|
+
const cacheWrite = event.usage?.cacheWrite ?? 0;
|
|
88
|
+
// Try multiple paths — OpenClaw may expose it under different field names
|
|
89
|
+
const thinkingTokens = event.usage?.thinking ??
|
|
90
|
+
event.usage?.thoughtsTokenCount ??
|
|
91
|
+
event.usage?.reasoning ??
|
|
92
|
+
event?.rawResponse?.usageMetadata?.thoughtsTokenCount ??
|
|
93
|
+
0;
|
|
94
|
+
const cost = calculateCost({
|
|
95
|
+
model: event.model,
|
|
96
|
+
inputTokens: tokensIn,
|
|
97
|
+
outputTokens: tokensOut,
|
|
98
|
+
cacheReadTokens: cacheRead,
|
|
99
|
+
cacheWriteTokens: cacheWrite,
|
|
100
|
+
thinkingTokens,
|
|
101
|
+
});
|
|
102
|
+
const record = {
|
|
103
|
+
runId: event.runId,
|
|
104
|
+
sessionId: event.sessionId,
|
|
105
|
+
provider: event.provider,
|
|
106
|
+
model: event.model,
|
|
107
|
+
tokensIn,
|
|
108
|
+
tokensOut,
|
|
109
|
+
cacheRead,
|
|
110
|
+
cacheWrite,
|
|
111
|
+
thinkingTokens,
|
|
112
|
+
costUsd: cost.totalCost,
|
|
113
|
+
costSource: cost.priceSource,
|
|
114
|
+
costBreakdown: cost.priceSource === "table" ? {
|
|
115
|
+
input: cost.inputCost,
|
|
116
|
+
output: cost.outputCost,
|
|
117
|
+
cacheRead: cost.cacheReadCost,
|
|
118
|
+
cacheWrite: cost.cacheWriteCost,
|
|
119
|
+
thinking: cost.thinkingCost,
|
|
120
|
+
} : undefined,
|
|
121
|
+
timestamp: Date.now(),
|
|
122
|
+
};
|
|
123
|
+
const acc = getOrCreateSession(event.sessionId);
|
|
124
|
+
acc.records.push(record);
|
|
125
|
+
if (config.debug) {
|
|
126
|
+
logger.info(`[deepclaw] llm_output session=${event.sessionId} model=${event.model} ` +
|
|
127
|
+
`in=${tokensIn} out=${tokensOut} cacheRead=${cacheRead} thinking=${thinkingTokens} ` +
|
|
128
|
+
`cost=$${record.costUsd.toFixed(6)} source=${record.costSource}`);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
// Flush & close on session end
|
|
132
|
+
api.on("session_end", async (event) => {
|
|
133
|
+
await flushSession(event.sessionId, "session_end");
|
|
134
|
+
});
|
|
135
|
+
// Start periodic flush timer
|
|
136
|
+
flushTimer = setInterval(() => {
|
|
137
|
+
flushAll("periodic").catch((err) => {
|
|
138
|
+
logger.warn(`[deepclaw] periodic flush error: ${String(err)}`);
|
|
139
|
+
});
|
|
140
|
+
}, config.flushIntervalMs);
|
|
141
|
+
},
|
|
142
|
+
async shutdown() {
|
|
143
|
+
if (flushTimer)
|
|
144
|
+
clearInterval(flushTimer);
|
|
145
|
+
// Final flush of all open sessions
|
|
146
|
+
const ids = Array.from(sessions.keys());
|
|
147
|
+
await Promise.all(ids.map((id) => flushSession(id, "session_end")));
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
"id": "deepclaw-openclaw",
|
|
3
3
|
"name": "DeepClaw",
|
|
4
4
|
"description": "Export real-time LLM cost & usage data to DeepClaw analytics",
|
|
5
|
+
"activation": {
|
|
6
|
+
"onStartup": true
|
|
7
|
+
},
|
|
5
8
|
"configSchema": {
|
|
6
9
|
"type": "object",
|
|
7
10
|
"additionalProperties": false,
|
|
8
|
-
"required": ["syncToken"],
|
|
9
11
|
"properties": {
|
|
10
12
|
"enabled": { "type": "boolean" },
|
|
11
13
|
"apiUrl": { "type": "string" },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deepclaw-openclaw",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "DeepClaw observability plugin for OpenClaw — real-time LLM usage, token, cache, reasoning, and cost telemetry.",
|
|
6
6
|
"repository": {
|
|
@@ -18,12 +18,14 @@
|
|
|
18
18
|
"npm": ">=10"
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.build.json",
|
|
21
22
|
"lint": "tsc --noEmit",
|
|
22
23
|
"typecheck": "tsc --noEmit",
|
|
23
24
|
"test": "vitest run",
|
|
24
25
|
"smoke": "vitest run src/**/*.test.ts",
|
|
25
|
-
"
|
|
26
|
-
"
|
|
26
|
+
"check:clawhub": "node scripts/check-clawhub-version.mjs",
|
|
27
|
+
"ci": "npm run typecheck && npm test && npm run build && npm pack --dry-run",
|
|
28
|
+
"prepack": "npm run typecheck && npm test && npm run build"
|
|
27
29
|
},
|
|
28
30
|
"keywords": [
|
|
29
31
|
"ai-gateway",
|
|
@@ -54,6 +56,7 @@
|
|
|
54
56
|
"openclaw": "^2026.4.24"
|
|
55
57
|
},
|
|
56
58
|
"files": [
|
|
59
|
+
"dist/**",
|
|
57
60
|
"index.ts",
|
|
58
61
|
"src/config.ts",
|
|
59
62
|
"src/pricing.ts",
|
|
@@ -73,7 +76,7 @@
|
|
|
73
76
|
"minGatewayVersion": "2026.3.2"
|
|
74
77
|
},
|
|
75
78
|
"build": {
|
|
76
|
-
"openclawVersion": "2026.
|
|
79
|
+
"openclawVersion": "2026.5.6"
|
|
77
80
|
},
|
|
78
81
|
"tags": [
|
|
79
82
|
"deepclaw",
|
|
@@ -84,6 +87,9 @@
|
|
|
84
87
|
],
|
|
85
88
|
"extensions": [
|
|
86
89
|
"./index.ts"
|
|
90
|
+
],
|
|
91
|
+
"runtimeExtensions": [
|
|
92
|
+
"./dist/index.js"
|
|
87
93
|
]
|
|
88
94
|
},
|
|
89
95
|
"publishConfig": {
|