deepclaw-openclaw 0.1.2 → 0.1.4
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 +6 -0
- 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/index.ts +2 -2
- package/openclaw.plugin.json +3 -1
- package/package.json +14 -8
- package/src/service.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented here.
|
|
4
4
|
|
|
5
|
+
## 0.1.4 - OpenClaw 2026.9 compatibility
|
|
6
|
+
|
|
7
|
+
- Import plugin APIs from the supported `openclaw/plugin-sdk/core` entrypoint.
|
|
8
|
+
- Build and test against OpenClaw 2026.9.4 on Node.js 26.
|
|
9
|
+
- Keep compatibility with gateway releases from 2026.5.6 onward and the deployed 2026.7.1-2 build.
|
|
10
|
+
|
|
5
11
|
## 0.1.2 - GitHub Packages publishing prep
|
|
6
12
|
|
|
7
13
|
- Add GitHub Actions workflow support for publishing to npmjs and GitHub Packages.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
2
|
+
declare const plugin: {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
configSchema: import("openclaw/plugin-sdk/core").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/core";
|
|
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/core";
|
|
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/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// DeepClaw OpenClaw Plugin — real-time LLM cost & usage tracking
|
|
2
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
3
|
-
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
3
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core";
|
|
4
4
|
import { createDeepClawService } from "./src/service.js";
|
|
5
5
|
import { parseDeepClawConfig } from "./src/config.js";
|
|
6
6
|
|
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.4",
|
|
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",
|
|
@@ -44,16 +46,17 @@
|
|
|
44
46
|
],
|
|
45
47
|
"dependencies": {},
|
|
46
48
|
"peerDependencies": {
|
|
47
|
-
"openclaw": ">=2026.
|
|
49
|
+
"openclaw": "2026.7.1-2 || >=2026.5.6"
|
|
48
50
|
},
|
|
49
51
|
"devDependencies": {
|
|
50
52
|
"@types/node": "^22.0.0",
|
|
51
53
|
"tsx": "^4.21.0",
|
|
52
54
|
"typescript": "^5.4.0",
|
|
53
55
|
"vitest": "^4.1.5",
|
|
54
|
-
"openclaw": "
|
|
56
|
+
"openclaw": "2026.9.4"
|
|
55
57
|
},
|
|
56
58
|
"files": [
|
|
59
|
+
"dist/**",
|
|
57
60
|
"index.ts",
|
|
58
61
|
"src/config.ts",
|
|
59
62
|
"src/pricing.ts",
|
|
@@ -69,11 +72,11 @@
|
|
|
69
72
|
"type": "plugin",
|
|
70
73
|
"category": "observability",
|
|
71
74
|
"compat": {
|
|
72
|
-
"pluginApi": ">=2026.
|
|
73
|
-
"minGatewayVersion": "2026.
|
|
75
|
+
"pluginApi": "2026.7.1-2 || >=2026.5.6",
|
|
76
|
+
"minGatewayVersion": "2026.5.6"
|
|
74
77
|
},
|
|
75
78
|
"build": {
|
|
76
|
-
"openclawVersion": "2026.4
|
|
79
|
+
"openclawVersion": "2026.9.4"
|
|
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": {
|
package/src/service.ts
CHANGED